#include "flecs.h"
/**
 * @file private_api.h
 * @brief Private functions.
 */

#ifndef FLECS_PRIVATE_H
#define FLECS_PRIVATE_H

/**
 * @file private_api.h
 * @brief Private types.
 */

#ifndef FLECS_PRIVATE_TYPES_H
#define FLECS_PRIVATE_TYPES_H

#ifndef __MACH__
#ifndef _POSIX_C_SOURCE
#define _POSIX_C_SOURCE 200809L
#endif
#endif

#include <stdlib.h>
#include <limits.h>
#ifndef FLECS_NDEBUG
#include <stdio.h> /* easier debugging, throws warning in release for printfs */
#endif

/**
 * @file entity_index.h
 * @brief Entity index data structure.
 *
 * The entity index stores the table, row for an entity id. It is implemented as
 * a sparse set. This file contains convenience macros for working with the
 * entity index.
 */

#ifndef FLECS_ENTITY_INDEX_H
#define FLECS_ENTITY_INDEX_H

#define ecs_eis(world) (&((world)->store.entity_index))
#define flecs_entities_get(world, entity) flecs_sparse_get(ecs_eis(world), ecs_record_t, entity)
#define flecs_entities_get_any(world, entity) flecs_sparse_get_any(ecs_eis(world), ecs_record_t, entity)
#define flecs_entities_set(world, entity, ...) (flecs_sparse_set(ecs_eis(world), ecs_record_t, entity, (__VA_ARGS__)))
#define flecs_entities_ensure(world, entity) flecs_sparse_ensure(ecs_eis(world), ecs_record_t, entity)
#define flecs_entities_remove(world, entity) flecs_sparse_remove(ecs_eis(world), entity)
#define flecs_entities_set_generation(world, entity) flecs_sparse_set_generation(ecs_eis(world), entity)
#define flecs_entities_is_alive(world, entity) flecs_sparse_is_alive(ecs_eis(world), entity)
#define flecs_entities_get_current(world, entity) flecs_sparse_get_alive(ecs_eis(world), entity)
#define flecs_entities_exists(world, entity) flecs_sparse_exists(ecs_eis(world), entity)
#define flecs_entities_recycle(world) flecs_sparse_new_id(ecs_eis(world))
#define flecs_entities_clear_entity(world, entity, is_watched) flecs_entities_set(ecs_eis(world), entity, &(ecs_record_t){NULL, is_watched})
#define flecs_entities_set_size(world, size) flecs_sparse_set_size(ecs_eis(world), size)
#define flecs_entities_count(world) flecs_sparse_count(ecs_eis(world))
#define flecs_entities_clear(world) flecs_sparse_clear(ecs_eis(world))
#define flecs_entities_copy(world) flecs_sparse_copy(ecs_eis(world))
#define flecs_entities_free(world) flecs_sparse_free(ecs_eis(world))
#define flecs_entities_memory(world, allocd, used) flecs_sparse_memory(ecs_eis(world), allocd, used)

#endif

/**
 * @file bitset.h
 * @brief Bitset datastructure.
 *
 * Simple bitset implementation. The bitset allows for storage of arbitrary
 * numbers of bits.
 */

#ifndef FLECS_BITSET_H
#define FLECS_BITSET_H


#ifdef __cplusplus
extern "C" {
#endif

typedef struct ecs_bitset_t {
    uint64_t *data;
    int32_t count;
    ecs_size_t size;
} ecs_bitset_t;

/** Initialize bitset. */
FLECS_DBG_API
void flecs_bitset_init(
    ecs_bitset_t *bs);

/** Deinialize bitset. */
FLECS_DBG_API
void flecs_bitset_fini(
    ecs_bitset_t *bs);

/** Add n elements to bitset. */
FLECS_DBG_API
void flecs_bitset_addn(
    ecs_bitset_t *bs,
    int32_t count);

/** Ensure element exists. */
FLECS_DBG_API
void flecs_bitset_ensure(
    ecs_bitset_t *bs,
    int32_t count);

/** Set element. */
FLECS_DBG_API
void flecs_bitset_set(
    ecs_bitset_t *bs,
    int32_t elem,
    bool value);

/** Get element. */
FLECS_DBG_API
bool flecs_bitset_get(
    const ecs_bitset_t *bs,
    int32_t elem);

/** Return number of elements. */
FLECS_DBG_API
int32_t flecs_bitset_count(
    const ecs_bitset_t *bs);

/** Remove from bitset. */
FLECS_DBG_API
void flecs_bitset_remove(
    ecs_bitset_t *bs,
    int32_t elem);

/** Swap values in bitset. */
FLECS_DBG_API
void flecs_bitset_swap(
    ecs_bitset_t *bs,
    int32_t elem_a,
    int32_t elem_b);

#ifdef __cplusplus
}
#endif

#endif

/**
 * @file switch_list.h
 * @brief Interleaved linked list for storing mutually exclusive values.
 *
 * Datastructure that stores N interleaved linked lists in an array. 
 * This allows for efficient storage of elements with mutually exclusive values.
 * Each linked list has a header element which points to the index in the array
 * that stores the first node of the list. Each list node points to the next
 * array element.
 *
 * The datastructure needs to be created with min and max values, so that it can
 * allocate an array of headers that can be directly indexed by the value. The
 * values are stored in a contiguous array, which allows for the values to be
 * iterated without having to follow the linked list nodes.
 *
 * The datastructure allows for efficient storage and retrieval for values with
 * mutually exclusive values, such as enumeration values. The linked list allows
 * an application to obtain all elements for a given (enumeration) value without
 * having to search.
 *
 * While the list accepts 64 bit values, it only uses the lower 32bits of the
 * value for selecting the correct linked list.
 */

#ifndef FLECS_SWITCH_LIST_H
#define FLECS_SWITCH_LIST_H


typedef struct ecs_switch_header_t {
    int32_t element;        /* First element for value */
    int32_t count;          /* Number of elements for value */
} ecs_switch_header_t;

typedef struct ecs_switch_node_t {
    int32_t next;           /* Next node in list */
    int32_t prev;           /* Prev node in list */
} ecs_switch_node_t;

struct ecs_switch_t {
    // uint64_t min;           /* Minimum value the switch can store */
    // uint64_t max;           /* Maximum value the switch can store */
    // ecs_switch_header_t *headers;   /* Array with headers, indexed by value */
    
    ecs_map_t headers;
    ecs_vector_t *nodes;    /* Vector with nodes, of type ecs_switch_node_t */
    ecs_vector_t *values;   /* Vector with values, of type uint64_t */
};

/** Init new switch. */
FLECS_DBG_API
void flecs_switch_init(
    ecs_switch_t* sw,
    int32_t elements);

/** Create new switch. */
FLECS_DBG_API
ecs_switch_t* flecs_switch_new(
    int32_t elements);

/** Fini switch. */
FLECS_DBG_API
void flecs_switch_fini(
    ecs_switch_t *sw);

/** Free switch. */
FLECS_DBG_API
void flecs_switch_free(
    ecs_switch_t *sw);

/** Remove all values. */
FLECS_DBG_API
void flecs_switch_clear(
    ecs_switch_t *sw);

/** Add element to switch, initialize value to 0 */
FLECS_DBG_API
void flecs_switch_add(
    ecs_switch_t *sw);

/** Set number of elements in switch list */
FLECS_DBG_API
void flecs_switch_set_count(
    ecs_switch_t *sw,
    int32_t count);

/** Get number of elements */
FLECS_DBG_API
int32_t flecs_switch_count(
    ecs_switch_t *sw);

/** Ensure that element exists. */
FLECS_DBG_API
void flecs_switch_ensure(
    ecs_switch_t *sw,
    int32_t count);

/** Add n elements. */
FLECS_DBG_API
void flecs_switch_addn(
    ecs_switch_t *sw,
    int32_t count);    

/** Set value of element. */
FLECS_DBG_API
void flecs_switch_set(
    ecs_switch_t *sw,
    int32_t element,
    uint64_t value);

/** Remove element. */
FLECS_DBG_API
void flecs_switch_remove(
    ecs_switch_t *sw,
    int32_t element);

/** Get value for element. */
FLECS_DBG_API
uint64_t flecs_switch_get(
    const ecs_switch_t *sw,
    int32_t element);

/** Swap element. */
FLECS_DBG_API
void flecs_switch_swap(
    ecs_switch_t *sw,
    int32_t elem_1,
    int32_t elem_2);

/** Get vector with all values. Use together with count(). */
FLECS_DBG_API
ecs_vector_t* flecs_switch_values(
    const ecs_switch_t *sw);    

/** Return number of different values. */
FLECS_DBG_API
int32_t flecs_switch_case_count(
    const ecs_switch_t *sw,
    uint64_t value);

/** Return first element for value. */
FLECS_DBG_API
int32_t flecs_switch_first(
    const ecs_switch_t *sw,
    uint64_t value);

/** Return next element for value. Use with first(). */
FLECS_DBG_API
int32_t flecs_switch_next(
    const ecs_switch_t *sw,
    int32_t elem);

#ifdef __cplusplus
extern "C" {
#endif

#ifdef __cplusplus
}
#endif

#endif


#define ECS_MAX_JOBS_PER_WORKER (16)

/* Magic number for a flecs object */
#define ECS_OBJECT_MAGIC (0x6563736f)

/* Tags associated with poly for (Poly, tag) components */
#define ecs_world_t_tag     invalid
#define ecs_stage_t_tag     invalid
#define ecs_query_t_tag     EcsQuery
#define ecs_rule_t_tag      invalid
#define ecs_table_t_tag     invalid
#define ecs_filter_t_tag    invalid
#define ecs_observer_t_tag  EcsObserver

/* Mixin kinds */
typedef enum ecs_mixin_kind_t {
    EcsMixinWorld,
    EcsMixinEntity,
    EcsMixinObservable,
    EcsMixinIterable,
    EcsMixinDtor,
    EcsMixinBase,        /* If mixin can't be found in object, look in base */
    EcsMixinMax
} ecs_mixin_kind_t;

/* The mixin array contains pointers to mixin members for different kinds of
 * flecs objects. This allows the API to retrieve data from an object regardless
 * of its type. Each mixin array is only stored once per type */
struct ecs_mixins_t {
    const char *type_name; /* Include name of mixin type so debug code doesn't
                            * need to know about every object */
    ecs_size_t elems[EcsMixinMax];                        
};

/* Mixin tables */
extern ecs_mixins_t ecs_world_t_mixins;
extern ecs_mixins_t ecs_stage_t_mixins;
extern ecs_mixins_t ecs_filter_t_mixins;
extern ecs_mixins_t ecs_query_t_mixins;
extern ecs_mixins_t ecs_trigger_t_mixins;
extern ecs_mixins_t ecs_observer_t_mixins;

/* Types that have no mixins */
#define ecs_table_t_mixins (&(ecs_mixins_t){ NULL })

/* Scope for flecs internals, like observers used for builtin features */
extern const ecs_entity_t EcsFlecsInternals;

/** Type used for internal string hashmap */
typedef struct ecs_hashed_string_t {
    char *value;
    ecs_size_t length;
    uint64_t hash;
} ecs_hashed_string_t;

/* Table event type for notifying tables of world events */
typedef enum ecs_table_eventkind_t {
    EcsTableTriggersForId,
    EcsTableNoTriggersForId,
} ecs_table_eventkind_t;

typedef struct ecs_table_event_t {
    ecs_table_eventkind_t kind;

    /* Query event */
    ecs_query_t *query;

    /* Component info event */
    ecs_entity_t component;

    /* Event match */
    ecs_entity_t event;

    /* If the nubmer of fields gets out of hand, this can be turned into a union
     * but since events are very temporary objects, this works for now and makes
     * initializing an event a bit simpler. */
} ecs_table_event_t;

/** A component column. */
struct ecs_column_t {
    void *array;
    int32_t count;
    int32_t size;
};

/** Stage-specific component data */
struct ecs_data_t {
    ecs_column_t entities;       /* Entity identifiers */
    ecs_column_t records;    /* Ptrs to records in main entity index */
    ecs_column_t *columns;       /* Component columns */
    ecs_switch_t *sw_columns;    /* Switch columns */
    ecs_bitset_t *bs_columns;    /* Bitset columns */
};

/** Cache of added/removed components for non-trivial edges between tables */
typedef struct ecs_table_diff_t {
    ecs_type_t added;         /* Components added between tables */
    ecs_type_t removed;       /* Components removed between tables */
    ecs_type_t on_set;        /* OnSet from exposing/adding base components */
    ecs_type_t un_set;        /* UnSet from hiding/removing base components */
} ecs_table_diff_t;

/** Edge linked list (used to keep track of incoming edges) */
typedef struct ecs_graph_edge_hdr_t {
    struct ecs_graph_edge_hdr_t *prev;
    struct ecs_graph_edge_hdr_t *next;
} ecs_graph_edge_hdr_t;

/** Single edge. */
typedef struct ecs_graph_edge_t {
    ecs_graph_edge_hdr_t hdr;
    ecs_table_t *from;       /* Edge source table */
    ecs_table_t *to;         /* Edge destination table */
    ecs_table_diff_t *diff;  /* Index into diff vector, if non trivial edge */
    ecs_id_t id;             /* Id associated with edge */
} ecs_graph_edge_t;

/* Edges to other tables. */
typedef struct ecs_graph_edges_t {
    ecs_graph_edge_t *lo; /* Small array optimized for low edges */
    ecs_map_t hi;  /* Map for hi edges (map<id, edge_t>) */
} ecs_graph_edges_t;

/* Table graph node */
typedef struct ecs_graph_node_t {
    /* Outgoing edges */
    ecs_graph_edges_t add;    
    ecs_graph_edges_t remove; 

    /* Incoming edges (next = add edges, prev = remove edges) */
    ecs_graph_edge_hdr_t refs;
} ecs_graph_node_t;

/** A table is the Flecs equivalent of an archetype. Tables store all entities
 * with a specific set of components. Tables are automatically created when an
 * entity has a set of components not previously observed before. When a new
 * table is created, it is automatically matched with existing queries */
struct ecs_table_t {
    uint64_t id;                     /* Table id in sparse set */
    ecs_type_t type;                 /* Identifies table type in type_index */
    ecs_flags32_t flags;             /* Flags for testing table properties */
    uint16_t storage_count;          /* Number of components (excluding tags) */
    uint16_t generation;             /* Used for table cleanup */

    struct ecs_table_record_t *records; /* Array with table records */
    ecs_table_t *storage_table;      /* Table without tags */
    ecs_id_t *storage_ids;           /* Component ids (prevent indirection) */
    int32_t *storage_map;            /* Map type <-> data type
                                      *  - 0..count(T):         type -> data_type
                                      *  - count(T)..count(S):  data_type -> type
                                      */

    ecs_graph_node_t node;           /* Graph node */
    ecs_data_t data;                 /* Component storage */
    ecs_type_info_t **type_info;     /* Cached type info */

    int32_t *dirty_state;            /* Keep track of changes in columns */

    int16_t sw_count;
    int16_t sw_offset;
    int16_t bs_count;
    int16_t bs_offset;

    int32_t refcount;                /* Increased when used as storage table */
    int32_t lock;                    /* Prevents modifications */
    uint16_t record_count;           /* Table record count including wildcards */
};

/** Must appear as first member in payload of table cache */
typedef struct ecs_table_cache_hdr_t {
    struct ecs_table_cache_t *cache;
    ecs_table_t *table;
    struct ecs_table_cache_hdr_t *prev, *next;
    bool empty;
} ecs_table_cache_hdr_t;

/** Linked list of tables in table cache */
typedef struct ecs_table_cache_list_t {
    ecs_table_cache_hdr_t *first;
    ecs_table_cache_hdr_t *last;
    int32_t count;
} ecs_table_cache_list_t;

/** Table cache */
typedef struct ecs_table_cache_t {
    ecs_map_t index; /* <table_id, T*> */
    ecs_table_cache_list_t tables;
    ecs_table_cache_list_t empty_tables;
} ecs_table_cache_t;

/* Sparse query column */
typedef struct flecs_switch_term_t {
    ecs_switch_t *sw_column;
    ecs_entity_t sw_case; 
    int32_t signature_column_index;
} flecs_switch_term_t;

/* Bitset query column */
typedef struct flecs_bitset_term_t {
    ecs_bitset_t *bs_column;
    int32_t column_index;
} flecs_bitset_term_t;

typedef struct ecs_query_table_match_t ecs_query_table_match_t;

/** List node used to iterate tables in a query.
 * The list of nodes dictates the order in which tables should be iterated by a
 * query. A single node may refer to the table multiple times with different
 * offset/count parameters, which enables features such as sorting. */
struct ecs_query_table_node_t {
    ecs_query_table_match_t *match;  /* Reference to the match */
    int32_t offset;                  /* Starting point in table  */
    int32_t count;                   /* Number of entities to iterate in table */
    ecs_query_table_node_t *next, *prev;
};

/** Type containing data for a table matched with a query. 
 * A single table can have multiple matches, if the query contains wildcards. */
struct ecs_query_table_match_t {
    ecs_query_table_node_t node; /* Embedded list node */

    ecs_table_t *table;       /* The current table. */
    int32_t *columns;         /* Mapping from query terms to table columns */
    ecs_id_t *ids;            /* Resolved (component) ids for current table */
    ecs_entity_t *sources;   /* Subjects (sources) of ids */
    ecs_size_t *sizes;        /* Sizes for ids for current table */
    ecs_ref_t *references;    /* Cached components for non-this terms */

    ecs_vector_t *sparse_columns;  /* Column ids of sparse columns */
    ecs_vector_t *bitset_columns;  /* Column ids with disabled flags */

    uint64_t group_id;        /* Value used to organize tables in groups */

    /* Next match in cache for same table (includes empty tables) */
    ecs_query_table_match_t *next_match;

    int32_t *monitor;                /* Used to monitor table for changes */
};

/** A single table can occur multiple times in the cache when a term matches
 * multiple table columns. */
typedef struct ecs_query_table_t {
    ecs_table_cache_hdr_t hdr;       /* Header for ecs_table_cache_t */
    ecs_query_table_match_t *first;  /* List with matches for table */
    ecs_query_table_match_t *last;   /* Last discovered match for table */
    int32_t rematch_count;           /* Track whether table was rematched */
} ecs_query_table_t;

/** Points to the beginning & ending of a query group */
typedef struct ecs_query_table_list_t {
    ecs_query_table_node_t *first;
    ecs_query_table_node_t *last;
    int32_t count;
} ecs_query_table_list_t;

/* Query event type for notifying queries of world events */
typedef enum ecs_query_eventkind_t {
    EcsQueryTableMatch,
    EcsQueryTableRematch,
    EcsQueryTableUnmatch,
    EcsQueryOrphan
} ecs_query_eventkind_t;

typedef struct ecs_query_event_t {
    ecs_query_eventkind_t kind;
    ecs_table_t *table;
    ecs_query_t *parent_query;
} ecs_query_event_t;

/** Query that is automatically matched against tables */
struct ecs_query_t {
    ecs_header_t hdr;

    /* Query filter */
    ecs_filter_t filter;

    /* Tables matched with query */
    ecs_table_cache_t cache;

    /* Linked list with all matched non-empty tables, in iteration order */
    ecs_query_table_list_t list;

    /* Contains head/tail to nodes of query groups (if group_by is used) */
    ecs_map_t groups;

    /* Used for sorting */
    ecs_entity_t order_by_component;
    ecs_order_by_action_t order_by;
    ecs_sort_table_action_t sort_table;
    ecs_vector_t *table_slices;

    /* Used for grouping */
    ecs_entity_t group_by_id;
    ecs_group_by_action_t group_by;
    void *group_by_ctx;
    ecs_ctx_free_t group_by_ctx_free;

    /* Subqueries */
    ecs_query_t *parent;
    ecs_vector_t *subqueries;

    /* Flags for query properties */
    ecs_flags32_t flags;

    int32_t cascade_by;         /* Identify cascade column */
    int32_t match_count;        /* How often have tables been (un)matched */
    int32_t prev_match_count;   /* Track if sorting is needed */
    int32_t rematch_count;      /* Track which tables were added during rematch */

    /* Mixins */
    ecs_world_t *world;
    ecs_iterable_t iterable;
    ecs_poly_dtor_t dtor;
    ecs_entity_t entity;
};

/** All observers for a specific (component) id */
typedef struct ecs_event_id_record_t {
    /* Triggers for Self */
    ecs_map_t observers; /* map<trigger_id, trigger_t> */

    /* Triggers for SuperSet, SubSet */
    ecs_map_t set_observers; /* map<trigger_id, trigger_t> */

    /* Triggers for Self with non-This subject */
    ecs_map_t entity_observers; /* map<trigger_id, trigger_t> */

    /* Number of active observers for (component) id */
    int32_t observer_count;
} ecs_event_id_record_t;

/** All observers for a specific event */
typedef struct ecs_event_record_t {
    ecs_map_t event_ids;     /* map<id, ecs_event_id_record_t> */
} ecs_event_record_t;

/** Types for deferred operations */
typedef enum ecs_defer_op_kind_t {
    EcsOpNew,
    EcsOpClone,
    EcsOpBulkNew,
    EcsOpAdd,
    EcsOpRemove,   
    EcsOpSet,
    EcsOpMut,
    EcsOpModified,
    EcsOpDelete,
    EcsOpClear,
    EcsOpOnDeleteAction,
    EcsOpEnable,
    EcsOpDisable
} ecs_defer_op_kind_t;

typedef struct ecs_defer_op_1_t {
    ecs_entity_t entity;        /* Entity id */
    void *value;                /* Component value (used by set / get_mut) */
    ecs_size_t size;            /* Size of value */
    bool clone_value;           /* Clone entity with value (used for clone) */ 
} ecs_defer_op_1_t;

typedef struct ecs_defer_op_n_t {
    ecs_entity_t *entities;  
    int32_t count;
} ecs_defer_op_n_t;

typedef struct ecs_defer_op_t {
    ecs_defer_op_kind_t kind;         /* Operation kind */    
    ecs_id_t id;                /* (Component) id */
    union {
        ecs_defer_op_1_t _1;
        ecs_defer_op_n_t _n;
    } is;
} ecs_defer_op_t;

/** Stack allocator for quick allocation of small temporary values */
#define ECS_STACK_PAGE_SIZE (4096)

typedef struct ecs_stack_page_t {
    void *data;
    struct ecs_stack_page_t *next;
    ecs_size_t sp;
} ecs_stack_page_t;

typedef struct ecs_stack_t {
    ecs_stack_page_t first;
    ecs_stack_page_t *cur;
} ecs_stack_t;

/** A stage is a data structure in which delta's are stored until it is safe to
 * merge those delta's with the main world stage. A stage allows flecs systems
 * to arbitrarily add/remove/set components and create/delete entities while
 * iterating. Additionally, worker threads have their own stage that lets them
 * mutate the state of entities without requiring locks. */
struct ecs_stage_t {
    ecs_header_t hdr;

    int32_t id;                  /* Unique id that identifies the stage */

    /* Deferred command queue */
    int32_t defer;
    ecs_vector_t *defer_queue;
    ecs_stack_t defer_stack; /* Temp memory used by deferred commands */
    bool defer_suspend;

    ecs_world_t *thread_ctx;     /* Points to stage when a thread stage */
    ecs_world_t *world;          /* Reference to world */
    ecs_os_thread_t thread;      /* Thread handle (0 if no threading is used) */

    /* One-shot actions to be executed after the merge */
    ecs_vector_t *post_frame_actions;

    /* Namespacing */
    ecs_entity_t scope;          /* Entity of current scope */
    ecs_entity_t with;           /* Id to add by default to new entities */
    ecs_entity_t base;           /* Currently instantiated top-level base */
    ecs_entity_t *lookup_path;   /* Search path used by lookup operations */

    /* Properties */
    bool auto_merge;             /* Should this stage automatically merge? */
    bool asynchronous;           /* Is stage asynchronous? (write only) */
};

/* Component monitor */
typedef struct ecs_monitor_t {
    ecs_vector_t *queries;       /* vector<ecs_query_t*> */
    bool is_dirty;               /* Should queries be rematched? */
} ecs_monitor_t;

/* Component monitors */
typedef struct ecs_monitor_set_t {
    ecs_map_t monitors;          /* map<id, ecs_monitor_t> */
    bool is_dirty;               /* Should monitors be evaluated? */
} ecs_monitor_set_t;

/* Data stored for id marked for deletion */
typedef struct ecs_marked_id_t {
    ecs_id_record_t *idr;
    ecs_id_t id;
    ecs_entity_t action; /* Set explicitly for delete_with, remove_all */
} ecs_marked_id_t;

typedef struct ecs_store_t {
    /* Entity lookup */
    ecs_sparse_t entity_index;   /* sparse<entity, ecs_record_t> */

    /* Table lookup by id */
    ecs_sparse_t tables;         /* sparse<table_id, ecs_table_t> */

    /* Table lookup by hash */
    ecs_hashmap_t table_map;     /* hashmap<ecs_type_t, ecs_table_t*> */

    /* Root table */
    ecs_table_t root;

    /* Table edge cache */
    ecs_graph_edge_hdr_t *first_free;

    /* Records cache */
    ecs_vector_t *records;

    /* Stack of ids being deleted. */
    ecs_vector_t *marked_ids;    /* vector<ecs_marked_ids_t> */
} ecs_store_t;

/* fini actions */
typedef struct ecs_action_elem_t {
    ecs_fini_action_t action;
    void *ctx;
} ecs_action_elem_t;

/** The world stores and manages all ECS data. An application can have more than
 * one world, but data is not shared between worlds. */
struct ecs_world_t {
    ecs_header_t hdr;

    /* --  Type metadata -- */
    ecs_map_t id_index;          /* map<id, ecs_id_record_t*> */
    ecs_sparse_t *type_info;     /* sparse<type_id, type_info_t> */

    /* -- Cached handle to id records -- */
    ecs_id_record_t *idr_wildcard;
    ecs_id_record_t *idr_wildcard_wildcard;
    ecs_id_record_t *idr_any;
    ecs_id_record_t *idr_isa_wildcard;
    ecs_id_record_t *idr_childof_0;

    /* -- Mixins -- */
    ecs_world_t *self;
    ecs_observable_t observable;
    ecs_iterable_t iterable;

    /* Unique id per generated event used to prevent duplicate notifications */
    int32_t event_id;

    /* Is entity range checking enabled? */
    bool range_check_enabled;

    /* --  Data storage -- */
    ecs_store_t store;

    /* --  Pending table event buffers -- */
    ecs_sparse_t *pending_buffer;  /* sparse<table_id, ecs_table_t*> */
    ecs_sparse_t *pending_tables;  /* sparse<table_id, ecs_table_t*> */

    /* Used to track when cache needs to be updated */
    ecs_monitor_set_t monitors;    /* map<id, ecs_monitor_t> */

    /* -- Systems -- */
    ecs_entity_t pipeline;             /* Current pipeline */

    /* -- Identifiers -- */
    ecs_hashmap_t aliases;
    ecs_hashmap_t symbols;
    const char *name_prefix;     /* Remove prefix from C names in modules */

    /* -- Staging -- */
    ecs_stage_t *stages;         /* Stages */
    int32_t stage_count;         /* Number of stages */

    /* -- Multithreading -- */
    ecs_os_cond_t worker_cond;   /* Signal that worker threads can start */
    ecs_os_cond_t sync_cond;     /* Signal that worker thread job is done */
    ecs_os_mutex_t sync_mutex;   /* Mutex for job_cond */
    int32_t workers_running;     /* Number of threads running */
    int32_t workers_waiting;     /* Number of workers waiting on sync */

    /* -- Time management -- */
    ecs_time_t world_start_time; /* Timestamp of simulation start */
    ecs_time_t frame_start_time; /* Timestamp of frame start */
    ecs_ftime_t fps_sleep;       /* Sleep time to prevent fps overshoot */

    /* -- Metrics -- */
    ecs_world_info_t info;

    /* -- World flags -- */
    ecs_flags32_t flags;

    void *context;               /* Application context */
    ecs_vector_t *fini_actions;  /* Callbacks to execute when world exits */
};

#endif

/**
 * @file table_cache.h
 * @brief Data structure for fast table iteration/lookups.
 */

#ifndef FLECS_TABLE_CACHE_H_
#define FLECS_TABLE_CACHE_H_

void ecs_table_cache_init(
    ecs_table_cache_t *cache);

void ecs_table_cache_fini(
    ecs_table_cache_t *cache);

void ecs_table_cache_insert(
    ecs_table_cache_t *cache,
    const ecs_table_t *table,
    ecs_table_cache_hdr_t *result);

void ecs_table_cache_replace(
    ecs_table_cache_t *cache,
    const ecs_table_t *table,
    ecs_table_cache_hdr_t *elem);

void* ecs_table_cache_remove(
    ecs_table_cache_t *cache,
    const ecs_table_t *table,
    ecs_table_cache_hdr_t *elem);

void* ecs_table_cache_get(
    const ecs_table_cache_t *cache,
    const ecs_table_t *table);

bool ecs_table_cache_set_empty(
    ecs_table_cache_t *cache,
    const ecs_table_t *table,
    bool empty);

bool ecs_table_cache_is_empty(
    const ecs_table_cache_t *cache);

#define flecs_table_cache_count(cache) (cache)->tables.count
#define flecs_table_cache_empty_count(cache) (cache)->empty_tables.count

bool flecs_table_cache_iter(
    ecs_table_cache_t *cache,
    ecs_table_cache_iter_t *out);

bool flecs_table_cache_empty_iter(
    ecs_table_cache_t *cache,
    ecs_table_cache_iter_t *out);

bool flecs_table_cache_all_iter(
    ecs_table_cache_t *cache,
    ecs_table_cache_iter_t *out);

ecs_table_cache_hdr_t* _flecs_table_cache_next(
    ecs_table_cache_iter_t *it);

#define flecs_table_cache_next(it, T)\
    (ECS_CAST(T*, _flecs_table_cache_next(it)))

#endif

/**
 * @file id_record.h
 * @brief Index for looking up tables by (component) id.
 */

#ifndef FLECS_ID_RECORD_H
#define FLECS_ID_RECORD_H

/* Payload for id cache */
typedef struct ecs_table_record_t {
    ecs_table_cache_hdr_t hdr;  /* Table cache header */
    int32_t column;             /* First column where id occurs in table */
    int32_t count;              /* Number of times id occurs in table */
} ecs_table_record_t;

/* Linked list of id records */
typedef struct ecs_id_record_elem_t {
    struct ecs_id_record_t *prev, *next;
} ecs_id_record_elem_t;

/* Payload for id index which contains all datastructures for an id. */
struct ecs_id_record_t {
    /* Cache with all tables that contain the id. Must be first member. */
    ecs_table_cache_t cache; /* table_cache<ecs_table_record_t> */

    /* Flags for id */
    ecs_flags32_t flags;

    /* Refcount */
    int32_t refcount;

    /* Name lookup index (currently only used for ChildOf pairs) */
    ecs_hashmap_t *name_index;

    /* Cached pointer to type info for id, if id contains data. */
    const ecs_type_info_t *type_info;

    /* Id of record */
    ecs_id_t id;

    /* Parent id record. For pair records the parent is the (R, *) record. */
    ecs_id_record_t *parent;
    
    /* Lists for all id records that match a pair wildcard. The wildcard id
     * record is at the head of the list. */
    ecs_id_record_elem_t first;   /* (R, *) */
    ecs_id_record_elem_t second;  /* (*, O) */
    ecs_id_record_elem_t acyclic; /* (*, O) with only acyclic relationships */
};

/* Get id record for id */
ecs_id_record_t* flecs_id_record_get(
    const ecs_world_t *world,
    ecs_id_t id);

/* Get id record for id for searching.
 * Same as flecs_id_record_get, but replaces (R, *) with (Union, R) if R is a
 * union relationship. */
ecs_id_record_t* flecs_query_id_record_get(
    const ecs_world_t *world,
    ecs_id_t id);

/* Ensure id record for id */
ecs_id_record_t* flecs_id_record_ensure(
    ecs_world_t *world,
    ecs_id_t id);

/* Increase refcount of id record */
void flecs_id_record_claim(
    ecs_world_t *world,
    ecs_id_record_t *idr);

/* Decrease refcount of id record, delete if 0 */
int32_t flecs_id_record_release(
    ecs_world_t *world,
    ecs_id_record_t *idr);

/* Release all empty tables in id record */
void flecs_id_record_release_tables(
    ecs_world_t *world,
    ecs_id_record_t *idr);

/* Set (component) type info for id record */
bool flecs_id_record_set_type_info(
    ecs_world_t *world,
    ecs_id_record_t *idr,
    const ecs_type_info_t *ti);

/* Ensure id record has name index */
ecs_hashmap_t* flecs_id_name_index_ensure(
    ecs_world_t *world,
    ecs_id_t id);

/* Get name index for id record */
ecs_hashmap_t* flecs_id_name_index_get(
    const ecs_world_t *world,
    ecs_id_t id);

/* Find table record for id */
ecs_table_record_t* flecs_table_record_get(
    const ecs_world_t *world,
    const ecs_table_t *table,
    ecs_id_t id);

/* Find table record for id record */
const ecs_table_record_t* flecs_id_record_get_table(
    const ecs_id_record_t *idr,
    const ecs_table_t *table);

/* Cleanup all id records in world */
void flecs_fini_id_records(
    ecs_world_t *world);

#endif

/**
 * @file observable.h
 * @brief Functions for sending events.
 */

#ifndef FLECS_OBSERVABLE_H
#define FLECS_OBSERVABLE_H

void flecs_observable_init(
    ecs_observable_t *observable);

void flecs_observable_fini(
    ecs_observable_t *observable);

void flecs_observers_notify(
    ecs_iter_t *it,
    ecs_observable_t *observable,
    const ecs_type_t *ids,
    ecs_entity_t event);

void flecs_set_observers_notify(
    ecs_iter_t *it,
    ecs_observable_t *observable,
    const ecs_type_t *ids,
    ecs_entity_t event,
    ecs_id_t set_id);

bool flecs_check_observers_for_event(
    const ecs_poly_t *world,
    ecs_id_t id,
    ecs_entity_t event);

void flecs_observer_fini(
    ecs_observer_t *observer);

void flecs_emit( 
    ecs_world_t *world,
    ecs_world_t *stage,
    ecs_event_desc_t *desc);

#endif

/**
 * @file storage.h
 * @brief Storage API
 */

#ifndef FLECS_STORAGE_H
#define FLECS_STORAGE_H

void ecs_storage_init(
    ecs_column_t *storage,
    ecs_size_t size,
    int32_t elem_count);

#define ecs_storage_init_t(storage, T, elem_count) \
    ecs_storage_init(storage, ECS_SIZEOF(T), elem_count)

void ecs_storage_fini(
    ecs_column_t *storage);

int32_t ecs_storage_count(
    ecs_column_t *storage);

int32_t ecs_storage_size(
    ecs_column_t *storage);

void* ecs_storage_get(
    ecs_column_t *storage,
    ecs_size_t size,
    int32_t index);

#define ecs_storage_get_t(storage, T, index) \
    ECS_CAST(T*, ecs_storage_get(storage, ECS_SIZEOF(T), index))

void* ecs_storage_first(
    ecs_column_t *storage);

#define ecs_storage_first_t(storage, T) \
    ECS_CAST(T*, ecs_storage_first(storage))

void* ecs_storage_last(
    ecs_column_t *storage,
    ecs_size_t size);

#define ecs_storage_last_t(storage, T) \
    ECS_CAST(T*, ecs_storage_last(storage, ECS_SIZEOF(T)))

void* ecs_storage_append(
    ecs_column_t *storage,
    ecs_size_t size);

#define ecs_storage_append_t(storage, T) \
    ECS_CAST(T*, ecs_storage_append(storage, ECS_SIZEOF(T)))

void ecs_storage_remove(
    ecs_column_t *storage,
    ecs_size_t size,
    int32_t elem);

#define ecs_storage_remove_t(storage, T) \
    ECS_CAST(T*, ecs_storage_remove(storage, ECS_SIZEOF(T)))

void ecs_storage_remove_last(
    ecs_column_t *storage);

ecs_column_t ecs_storage_copy(
    ecs_column_t *storage,
    ecs_size_t size);

#define ecs_storage_copy_t(storage, T) \
    ecs_storage_copy(storage, ECS_SIZEOF(T))

void ecs_storage_reclaim(
    ecs_column_t *storage,
    ecs_size_t size);

#define ecs_storage_reclaim_t(storage, T) \
    ecs_storage_reclaim(storage, ECS_SIZEOF(T))

void ecs_storage_set_size(
    ecs_column_t *storage,
    ecs_size_t size,
    int32_t elem_count);

#define ecs_storage_set_size_t(storage, T, elem_count) \
    ecs_storage_set_size(storage, ECS_SIZEOF(T), elem_count)

void ecs_storage_set_count(
    ecs_column_t *storage,
    ecs_size_t size,
    int32_t elem_count);

#define ecs_storage_set_count_t(storage, T, elem_count) \
    ecs_storage_set_count(storage, ECS_SIZEOF(T), elem_count)

void* ecs_storage_grow(
    ecs_column_t *storage,
    ecs_size_t size,
    int32_t elem_count);

#define ecs_storage_grow_t(storage, T, elem_count) \
    ecs_storage_grow(storage, ECS_SIZEOF(T), elem_count)

#endif 

/**
 * @file iter.h
 * @brief Iterator utilities.
 */

#ifndef FLECS_ITER_H
#define FLECS_ITER_H

void flecs_iter_init(
    ecs_iter_t *it,
    ecs_flags8_t fields);

void flecs_iter_validate(
    ecs_iter_t *it);

void flecs_iter_populate_data(
    ecs_world_t *world,
    ecs_iter_t *it,
    ecs_table_t *table,
    int32_t offset,
    int32_t count,
    void **ptrs,
    ecs_size_t *sizes);

bool flecs_iter_next_row(
    ecs_iter_t *it);

bool flecs_iter_next_instanced(
    ecs_iter_t *it,
    bool result);

#endif

/**
 * @file table.h
 * @brief Table functions.
 */

#ifndef FLECS_TABLE_H
#define FLECS_TABLE_H

/* Init table */
void flecs_table_init(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_table_t *from);

/** Copy type. */
ecs_type_t flecs_type_copy(
    const ecs_type_t *src);

/** Free type. */
void flecs_type_free(
    ecs_type_t *type);

/** Find or create table for a set of components */
ecs_table_t* flecs_table_find_or_create(
    ecs_world_t *world,
    ecs_type_t *type);

/* Initialize columns for data */
void flecs_table_init_data(
    ecs_table_t *table); 

/* Clear all entities from a table. */
void flecs_table_clear_entities(
    ecs_world_t *world,
    ecs_table_t *table);

/* Reset a table to its initial state */
void flecs_table_reset(
    ecs_world_t *world,
    ecs_table_t *table);

/* Clear all entities from the table. Do not invoke OnRemove systems */
void flecs_table_clear_entities_silent(
    ecs_world_t *world,
    ecs_table_t *table);

/* Clear table data. Don't call OnRemove handlers. */
void flecs_table_clear_data(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_data_t *data);    

/* Return number of entities in data */
int32_t flecs_table_data_count(
    const ecs_data_t *data);

/* Add a new entry to the table for the specified entity */
int32_t flecs_table_append(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_entity_t entity,
    ecs_record_t *record,
    bool construct);

/* Delete an entity from the table. */
void flecs_table_delete(
    ecs_world_t *world,
    ecs_table_t *table,
    int32_t index,
    bool destruct);

/* Make sure table records are in correct table cache list */
bool flecs_table_records_update_empty(
    ecs_table_t *table);

/* Move a row from one table to another */
void flecs_table_move(
    ecs_world_t *world,
    ecs_entity_t dst_entity,
    ecs_entity_t src_entity,
    ecs_table_t *new_table,
    int32_t new_index,
    ecs_table_t *old_table,
    int32_t old_index,
    bool construct);

/* Grow table with specified number of records. Populate table with entities,
 * starting from specified entity id. */
int32_t flecs_table_appendn(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_data_t *data,
    int32_t count,
    const ecs_entity_t *ids);

/* Set table to a fixed size. Useful for preallocating memory in advance. */
void flecs_table_set_size(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_data_t *data,
    int32_t count);

/* Shrink table to contents */
bool flecs_table_shrink(
    ecs_world_t *world,
    ecs_table_t *table);

/* Get dirty state for table columns */
int32_t* flecs_table_get_dirty_state(
    ecs_table_t *table);

/* Get monitor for monitoring table changes */
int32_t* flecs_table_get_monitor(
    ecs_table_t *table);

/* Initialize root table */
void flecs_init_root_table(
    ecs_world_t *world);

/* Unset components in table */
void flecs_table_remove_actions(
    ecs_world_t *world,
    ecs_table_t *table);

/* Free table */
void flecs_table_free(
    ecs_world_t *world,
    ecs_table_t *table); 

/* Free table */
void flecs_table_free_type(
    ecs_table_t *table);     
    
/* Replace data */
void flecs_table_replace_data(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_data_t *data);

/* Merge data of one table into another table */
void flecs_table_merge(
    ecs_world_t *world,
    ecs_table_t *new_table,
    ecs_table_t *old_table,
    ecs_data_t *new_data,
    ecs_data_t *old_data);

void flecs_table_swap(
    ecs_world_t *world,
    ecs_table_t *table,
    int32_t row_1,
    int32_t row_2);

ecs_table_t *flecs_table_traverse_add(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_id_t *id_ptr,
    ecs_table_diff_t *diff);

ecs_table_t *flecs_table_traverse_remove(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_id_t *id_ptr,
    ecs_table_diff_t *diff);

void flecs_table_mark_dirty(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_entity_t component);

void flecs_table_notify(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_table_event_t *event);

void flecs_table_clear_edges(
    ecs_world_t *world,
    ecs_table_t *table);

void flecs_table_delete_entities(
    ecs_world_t *world,
    ecs_table_t *table);

ecs_column_t *ecs_table_column_for_id(
    const ecs_world_t *world,
    const ecs_table_t *table,
    ecs_id_t id);

int32_t flecs_table_column_to_union_index(
    const ecs_table_t *table,
    int32_t column);

/* Increase refcount of table (prevents deletion) */
void flecs_table_claim(
    ecs_world_t *world, 
    ecs_table_t *table);

/* Decreases refcount of table (may delete) */
bool flecs_table_release(
    ecs_world_t *world, 
    ecs_table_t *table);

#endif

/**
 * @file poly.h
 * @brief Functions for managing poly objects.
 * 
 * The poly framework makes it possible to generalize common functionality for
 * different kinds of API objects, as well as improved type safety checks. Poly
 * objects have a header that identifiers what kind of object it is. This can
 * then be used to discover a set of "mixins" implemented by the type.
 * 
 * Mixins are like a vtable, but for members. Each type populates the table with
 * offsets to the members that correspond with the mixin. If an entry in the
 * mixin table is not set, the type does not support the mixin.
 * 
 * An example is the Iterable mixin, which makes it possible to create an 
 * iterator for any poly object (like filters, queries, the world) that 
 * implements the Iterable mixin.
 */

#ifndef FLECS_POLY_H
#define FLECS_POLY_H

#include <stddef.h>

/* Initialize poly */
void* _ecs_poly_init(
    ecs_poly_t *object,
    int32_t kind,
    ecs_size_t size,
    ecs_mixins_t *mixins);

#define ecs_poly_init(object, type)\
    _ecs_poly_init(object, type##_magic, sizeof(type), &type##_mixins)

/* Deinitialize object for specified type */
void _ecs_poly_fini(
    ecs_poly_t *object,
    int32_t kind);

#define ecs_poly_fini(object, type)\
    _ecs_poly_fini(object, type##_magic)

/* Utility functions for creating an object on the heap */
#define ecs_poly_new(type)\
    (type*)ecs_poly_init(ecs_os_calloc_t(type), type)

#define ecs_poly_free(obj, type)\
    ecs_poly_fini(obj, type);\
    ecs_os_free(obj)

/* Get or create poly component for an entity */
EcsPoly* _ecs_poly_bind(
    ecs_world_t *world,
    ecs_entity_t entity,
    ecs_entity_t tag);

#define ecs_poly_bind(world, entity, T) \
    _ecs_poly_bind(world, entity, T##_tag)

void _ecs_poly_modified(
    ecs_world_t *world,
    ecs_entity_t entity,
    ecs_entity_t tag);

#define ecs_poly_modified(world, entity, T) \
    _ecs_poly_modified(world, entity, T##_tag)

/* Get poly component for an entity */
const EcsPoly* _ecs_poly_bind_get(
    const ecs_world_t *world,
    ecs_entity_t entity,
    ecs_entity_t tag);

#define ecs_poly_bind_get(world, entity, T) \
    _ecs_poly_bind_get(world, entity, T##_tag)

ecs_poly_t* _ecs_poly_get(
    const ecs_world_t *world,
    ecs_entity_t entity,
    ecs_entity_t tag);

#define ecs_poly_get(world, entity, T) \
    ((T*)_ecs_poly_get(world, entity, T##_tag))

/* Utilities for testing/asserting an object type */
#ifndef FLECS_NDEBUG
void* _ecs_poly_assert(
    const ecs_poly_t *object,
    int32_t type,
    const char *file,
    int32_t line);

#define ecs_poly_assert(object, type)\
    _ecs_poly_assert(object, type##_magic, __FILE__, __LINE__)

#define ecs_poly(object, T) ((T*)ecs_poly_assert(object, T))
#else
#define ecs_poly_assert(object, type)
#define ecs_poly(object, T) ((T*)object)
#endif

bool _ecs_poly_is(
    const ecs_poly_t *object,
    int32_t type);

#define ecs_poly_is(object, type)\
    _ecs_poly_is(object, type##_magic)

/* Utility functions for getting a mixin from an object */
ecs_iterable_t* ecs_get_iterable(
    const ecs_poly_t *poly);

ecs_observable_t* ecs_get_observable(
    const ecs_poly_t *object);

ecs_poly_dtor_t* ecs_get_dtor(
    const ecs_poly_t *poly);

#endif

/**
 * @file stage.h
 * @brief Stage functions.
 */

#ifndef FLECS_STAGE_H
#define FLECS_STAGE_H

/* Initialize stage data structures */
void flecs_stage_init(
    ecs_world_t *world,
    ecs_stage_t *stage);

/* Deinitialize stage */
void flecs_stage_deinit(
    ecs_world_t *world,
    ecs_stage_t *stage);

/* Post-frame merge actions */
void flecs_stage_merge_post_frame(
    ecs_world_t *world,
    ecs_stage_t *stage);  

bool flecs_defer_begin(
    ecs_world_t *world,
    ecs_stage_t *stage);

bool flecs_defer_modified(
    ecs_world_t *world,
    ecs_stage_t *stage,
    ecs_entity_t entity,
    ecs_entity_t component);

bool flecs_defer_new(
    ecs_world_t *world,
    ecs_stage_t *stage,
    ecs_entity_t entity,
    ecs_id_t id);

bool flecs_defer_clone(
    ecs_world_t *world,
    ecs_stage_t *stage,
    ecs_entity_t entity,
    ecs_entity_t src,
    bool clone_value);

bool flecs_defer_bulk_new(
    ecs_world_t *world,
    ecs_stage_t *stage,
    int32_t count,
    ecs_id_t id,
    const ecs_entity_t **ids_out);

bool flecs_defer_delete(
    ecs_world_t *world,
    ecs_stage_t *stage,
    ecs_entity_t entity);

bool flecs_defer_clear(
    ecs_world_t *world,
    ecs_stage_t *stage,
    ecs_entity_t entity);

bool flecs_defer_on_delete_action(
    ecs_world_t *world,
    ecs_stage_t *stage,
    ecs_id_t id,
    ecs_entity_t action);

bool flecs_defer_enable(
    ecs_world_t *world,
    ecs_stage_t *stage,
    ecs_entity_t entity,
    ecs_entity_t component,
    bool enable);    

bool flecs_defer_add(
    ecs_world_t *world,
    ecs_stage_t *stage,
    ecs_entity_t entity,
    ecs_id_t id);

bool flecs_defer_remove(
    ecs_world_t *world,
    ecs_stage_t *stage,
    ecs_entity_t entity,
    ecs_id_t id);

bool flecs_defer_set(
    ecs_world_t *world,
    ecs_stage_t *stage,
    ecs_defer_op_kind_t op_kind,
    ecs_entity_t entity,
    ecs_entity_t component,
    ecs_size_t size,
    const void *value,
    void **value_out);

bool flecs_defer_end(
    ecs_world_t *world,
    ecs_stage_t *stage);

bool flecs_defer_purge(
    ecs_world_t *world,
    ecs_stage_t *stage);

#endif

/**
 * @file world.h
 * @brief World utilities.
 */

#ifndef FLECS_WORLD_H
#define FLECS_WORLD_H

/* Get current stage */
ecs_stage_t* flecs_stage_from_world(
    ecs_world_t **world_ptr);

/* Get current thread-specific stage from readonly world */
const ecs_stage_t* flecs_stage_from_readonly_world(
    const ecs_world_t *world);

/* Get component callbacks */
const ecs_type_info_t *flecs_type_info_get(
    const ecs_world_t *world,
    ecs_entity_t component);

/* Get or create component callbacks */
ecs_type_info_t* flecs_type_info_ensure(
    ecs_world_t *world,
    ecs_entity_t component);

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);

#define flecs_type_info_init(world, T, ...)\
    flecs_type_info_init_id(world, ecs_id(T), ECS_SIZEOF(T), ECS_ALIGNOF(T),\
        &(ecs_type_hooks_t)__VA_ARGS__)

void flecs_type_info_fini(
    ecs_type_info_t *ti);

void flecs_type_info_free(
    ecs_world_t *world,
    ecs_entity_t component);

void flecs_eval_component_monitors(
    ecs_world_t *world);

void flecs_monitor_mark_dirty(
    ecs_world_t *world,
    ecs_entity_t id);

void flecs_monitor_register(
    ecs_world_t *world,
    ecs_entity_t id,
    ecs_query_t *query);

void flecs_monitor_unregister(
    ecs_world_t *world,
    ecs_entity_t id,
    ecs_query_t *query);

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

void flecs_notify_queries(
    ecs_world_t *world,
    ecs_query_event_t *event);

void flecs_register_table(
    ecs_world_t *world,
    ecs_table_t *table);

void flecs_unregister_table(
    ecs_world_t *world,
    ecs_table_t *table);

void flecs_table_set_empty(
    ecs_world_t *world,
    ecs_table_t *table);

void flecs_delete_table(
    ecs_world_t *world,
    ecs_table_t *table);

void flecs_process_pending_tables(
    const ecs_world_t *world);

/* Suspend/resume readonly state. To fully support implicit registration of
 * components, it should be possible to register components while the world is
 * in readonly mode. It is not uncommon that a component is used first from
 * within a system, which are often ran while in readonly mode.
 * 
 * Suspending readonly mode is only allowed when the world is not multithreaded.
 * When a world is multithreaded, it is not safe to (even temporarily) leave
 * readonly mode, so a multithreaded application should always explicitly
 * register components in advance. 
 * 
 * These operations also suspend deferred mode.
 */
typedef struct {
    bool is_readonly;
    bool is_deferred;
    int32_t defer_count;
    ecs_entity_t scope;
    ecs_entity_t with;
    ecs_vector_t *defer_queue;
    ecs_stack_t defer_stack;
    ecs_stage_t *stage;
} ecs_suspend_readonly_state_t;

ecs_world_t* flecs_suspend_readonly(
    const ecs_world_t *world,
    ecs_suspend_readonly_state_t *state);

void flecs_resume_readonly(
    ecs_world_t *world,
    ecs_suspend_readonly_state_t *state);

#endif

/* From: https://github.com/svpv/qsort/blob/master/qsort.h 
 * Use custom qsort implementation rather than relying on the version in libc to
 * ensure that results are consistent across platforms.
 */

/*
 * Copyright (c) 2013, 2017 Alexey Tourbin
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

/*
 * This is a traditional Quicksort implementation which mostly follows
 * [Sedgewick 1978].  Sorting is performed entirely on array indices,
 * while actual access to the array elements is abstracted out with the
 * user-defined `LESS` and `SWAP` primitives.
 *
 * Synopsis:
 *	QSORT(N, LESS, SWAP);
 * where
 *	N - the number of elements in A[];
 *	LESS(i, j) - compares A[i] to A[j];
 *	SWAP(i, j) - exchanges A[i] with A[j].
 */

#ifndef QSORT_H
#define QSORT_H

/* Sort 3 elements. */
#define Q_SORT3(q_a1, q_a2, q_a3, Q_LESS, Q_SWAP) \
do {					\
    if (Q_LESS(q_a2, q_a1)) {		\
	if (Q_LESS(q_a3, q_a2))		\
	    Q_SWAP(q_a1, q_a3);		\
	else {				\
	    Q_SWAP(q_a1, q_a2);		\
	    if (Q_LESS(q_a3, q_a2))	\
		Q_SWAP(q_a2, q_a3);	\
	}				\
    }					\
    else if (Q_LESS(q_a3, q_a2)) {	\
	Q_SWAP(q_a2, q_a3);		\
	if (Q_LESS(q_a2, q_a1))		\
	    Q_SWAP(q_a1, q_a2);		\
    }					\
} while (0)

/* Partition [q_l,q_r] around a pivot.  After partitioning,
 * [q_l,q_j] are the elements that are less than or equal to the pivot,
 * while [q_i,q_r] are the elements greater than or equal to the pivot. */
#define Q_PARTITION(q_l, q_r, q_i, q_j, Q_UINT, Q_LESS, Q_SWAP)		\
do {									\
    /* The middle element, not to be confused with the median. */	\
    Q_UINT q_m = q_l + ((q_r - q_l) >> 1);				\
    /* Reorder the second, the middle, and the last items.		\
     * As [Edelkamp Weiss 2016] explain, using the second element	\
     * instead of the first one helps avoid bad behaviour for		\
     * decreasingly sorted arrays.  This method is used in recent	\
     * versions of gcc's std::sort, see gcc bug 58437#c13, although	\
     * the details are somewhat different (cf. #c14). */		\
    Q_SORT3(q_l + 1, q_m, q_r, Q_LESS, Q_SWAP);				\
    /* Place the median at the beginning. */				\
    Q_SWAP(q_l, q_m);							\
    /* Partition [q_l+2, q_r-1] around the median which is in q_l.	\
     * q_i and q_j are initially off by one, they get decremented	\
     * in the do-while loops. */					\
    q_i = q_l + 1; q_j = q_r;						\
    while (1) {								\
	do q_i++; while (Q_LESS(q_i, q_l));				\
	do q_j--; while (Q_LESS(q_l, q_j));				\
	if (q_i >= q_j) break; /* Sedgewick says "until j < i" */	\
	Q_SWAP(q_i, q_j);						\
    }									\
    /* Compensate for the i==j case. */					\
    q_i = q_j + 1;							\
    /* Put the median to its final place. */				\
    Q_SWAP(q_l, q_j);							\
    /* The median is not part of the left subfile. */			\
    q_j--;								\
} while (0)

/* Insertion sort is applied to small subfiles - this is contrary to
 * Sedgewick's suggestion to run a separate insertion sort pass after
 * the partitioning is done.  The reason I don't like a separate pass
 * is that it triggers extra comparisons, because it can't see that the
 * medians are already in their final positions and need not be rechecked.
 * Since I do not assume that comparisons are cheap, I also do not try
 * to eliminate the (q_j > q_l) boundary check. */
#define Q_INSERTION_SORT(q_l, q_r, Q_UINT, Q_LESS, Q_SWAP)		\
do {									\
    Q_UINT q_i, q_j;							\
    /* For each item starting with the second... */			\
    for (q_i = q_l + 1; q_i <= q_r; q_i++)				\
    /* move it down the array so that the first part is sorted. */	\
    for (q_j = q_i; q_j > q_l && (Q_LESS(q_j, q_j - 1)); q_j--)		\
	Q_SWAP(q_j, q_j - 1);						\
} while (0)

/* When the size of [q_l,q_r], i.e. q_r-q_l+1, is greater than or equal to
 * Q_THRESH, the algorithm performs recursive partitioning.  When the size
 * drops below Q_THRESH, the algorithm switches to insertion sort.
 * The minimum valid value is probably 5 (with 5 items, the second and
 * the middle items, the middle itself being rounded down, are distinct). */
#define Q_THRESH 16

/* The main loop. */
#define Q_LOOP(Q_UINT, Q_N, Q_LESS, Q_SWAP)				\
do {									\
    Q_UINT q_l = 0;							\
    Q_UINT q_r = (Q_N) - 1;						\
    Q_UINT q_sp = 0; /* the number of frames pushed to the stack */	\
    struct { Q_UINT q_l, q_r; }						\
	/* On 32-bit platforms, to sort a "char[3GB+]" array,		\
	 * it may take full 32 stack frames.  On 64-bit CPUs,		\
	 * though, the address space is limited to 48 bits.		\
	 * The usage is further reduced if Q_N has a 32-bit type. */	\
	q_st[sizeof(Q_UINT) > 4 && sizeof(Q_N) > 4 ? 48 : 32];		\
    while (1) {								\
	if (q_r - q_l + 1 >= Q_THRESH) {				\
	    Q_UINT q_i, q_j;						\
	    Q_PARTITION(q_l, q_r, q_i, q_j, Q_UINT, Q_LESS, Q_SWAP);	\
	    /* Now have two subfiles: [q_l,q_j] and [q_i,q_r].		\
	     * Dealing with them depends on which one is bigger. */	\
	    if (q_j - q_l >= q_r - q_i)					\
		Q_SUBFILES(q_l, q_j, q_i, q_r);				\
	    else							\
		Q_SUBFILES(q_i, q_r, q_l, q_j);				\
	}								\
	else {								\
	    Q_INSERTION_SORT(q_l, q_r, Q_UINT, Q_LESS, Q_SWAP);		\
	    /* Pop subfiles from the stack, until it gets empty. */	\
	    if (q_sp == 0) break;					\
	    q_sp--;							\
	    q_l = q_st[q_sp].q_l;					\
	    q_r = q_st[q_sp].q_r;					\
	}								\
    }									\
} while (0)

/* The missing part: dealing with subfiles.
 * Assumes that the first subfile is not smaller than the second. */
#define Q_SUBFILES(q_l1, q_r1, q_l2, q_r2)				\
do {									\
    /* If the second subfile is only a single element, it needs		\
     * no further processing.  The first subfile will be processed	\
     * on the next iteration (both subfiles cannot be only a single	\
     * element, due to Q_THRESH). */					\
    if (q_l2 == q_r2) {							\
	q_l = q_l1;							\
	q_r = q_r1;							\
    }									\
    else {								\
	/* Otherwise, both subfiles need processing.			\
	 * Push the larger subfile onto the stack. */			\
	q_st[q_sp].q_l = q_l1;						\
	q_st[q_sp].q_r = q_r1;						\
	q_sp++;								\
	/* Process the smaller subfile on the next iteration. */	\
	q_l = q_l2;							\
	q_r = q_r2;							\
    }									\
} while (0)

/* And now, ladies and gentlemen, may I proudly present to you... */
#define QSORT(Q_N, Q_LESS, Q_SWAP)					\
do {									\
    if ((Q_N) > 1)							\
	/* We could check sizeof(Q_N) and use "unsigned", but at least	\
	 * on x86_64, this has the performance penalty of up to 5%. */	\
	Q_LOOP(ecs_size_t, Q_N, Q_LESS, Q_SWAP);			\
} while (0)

void ecs_qsort(
    void *base, 
    ecs_size_t nitems, 
    ecs_size_t size, 
    int (*compar)(const void *, const void*));

#define ecs_qsort_t(base, nitems, T, compar) \
    ecs_qsort(base, nitems, ECS_SIZEOF(T), compar)

#endif

/**
 * @file name_index.h
 * @brief Data structure used for id <-> name lookups.
 */

#ifndef FLECS_NAME_INDEX_H
#define FLECS_NAME_INDEX_H

void flecs_name_index_init(
    ecs_hashmap_t *hm);

ecs_hashmap_t* flecs_name_index_new(void);

void flecs_name_index_fini(
    ecs_hashmap_t *map);

void flecs_name_index_free(
    ecs_hashmap_t *map);

ecs_hashed_string_t flecs_get_hashed_string(
    const char *name,
    ecs_size_t length,
    uint64_t hash);

const uint64_t* flecs_name_index_find_ptr(
    const ecs_hashmap_t *map,
    const char *name,
    ecs_size_t length,
    uint64_t hash);

uint64_t flecs_name_index_find(
    const ecs_hashmap_t *map,
    const char *name,
    ecs_size_t length,
    uint64_t hash);

void flecs_name_index_ensure(
    ecs_hashmap_t *map,
    uint64_t id,
    const char *name,
    ecs_size_t length,
    uint64_t hash);

void flecs_name_index_remove(
    ecs_hashmap_t *map,
    uint64_t id,
    uint64_t hash);

void flecs_name_index_update_name(
    ecs_hashmap_t *map,
    uint64_t e,
    uint64_t hash,
    const char *name);

#endif

/**
 * @file stack_allocator.h
 * @brief Data structure used for temporary small allocations.
 */

#ifndef FLECS_STACK_ALLOCATOR_H
#define FLECS_STACK_ALLOCATOR_H

void flecs_stack_init(
    ecs_stack_t *stack);

void flecs_stack_fini(
    ecs_stack_t *stack);

void* flecs_stack_alloc(
    ecs_stack_t *stack, 
    ecs_size_t size,
    ecs_size_t align);

void flecs_stack_free(
    void *ptr,
    ecs_size_t size);

void flecs_stack_reset(
    ecs_stack_t *stack);

#endif



////////////////////////////////////////////////////////////////////////////////
//// Bootstrap API
////////////////////////////////////////////////////////////////////////////////

/* Bootstrap world */
void flecs_bootstrap(
    ecs_world_t *world);

#define flecs_bootstrap_component(world, id_)\
    ecs_component_init(world, &(ecs_component_desc_t){\
        .entity = ecs_entity(world, { .id = ecs_id(id_), .name = #id_, .symbol = #id_ }),\
        .type.size = sizeof(id_),\
        .type.alignment = ECS_ALIGNOF(id_)\
    });

#define flecs_bootstrap_tag(world, name)\
    ecs_add_id(world, name, EcsFinal);\
    ecs_add_pair(world, name, EcsChildOf, ecs_get_scope(world));\
    ecs_set(world, name, EcsComponent, {.size = 0});\
    ecs_set_name(world, name, (char*)&#name[ecs_os_strlen(world->name_prefix)]);\
    ecs_set_symbol(world, name, #name)


/* Bootstrap functions for other parts in the code */
void flecs_bootstrap_hierarchy(ecs_world_t *world);


////////////////////////////////////////////////////////////////////////////////
//// Entity API
////////////////////////////////////////////////////////////////////////////////

/* Mark an entity as being watched. This is used to trigger automatic rematching
 * when entities used in system expressions change their components. */
void flecs_add_flag(
    ecs_world_t *world,
    ecs_entity_t entity,
    uint32_t flag);

ecs_entity_t flecs_get_oneof(
    const ecs_world_t *world,
    ecs_entity_t e);

void flecs_notify_on_remove(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_table_t *other_table,
    int32_t row,
    int32_t count,
    ecs_table_diff_t *diff);

void flecs_notify_on_set(
    ecs_world_t *world,
    ecs_table_t *table,
    int32_t row,
    int32_t count,
    ecs_type_t *type,
    bool owned);

int32_t flecs_relation_depth(
    const ecs_world_t *world,
    ecs_entity_t r,
    ecs_table_t *table);

////////////////////////////////////////////////////////////////////////////////
//// Query API
////////////////////////////////////////////////////////////////////////////////

/* Match table with term */
bool flecs_term_match_table(
    ecs_world_t *world,
    const ecs_term_t *term,
    const ecs_table_t *table,
    ecs_id_t *id_out,
    int32_t *column_out,
    ecs_entity_t *subject_out,
    int32_t *match_indices,
    bool first,
    ecs_flags32_t iter_flags);

/* Match table with filter */
bool flecs_filter_match_table(
    ecs_world_t *world,
    const ecs_filter_t *filter,
    const ecs_table_t *table,
    ecs_id_t *ids,
    int32_t *columns,
    ecs_entity_t *sources,
    int32_t *match_indices,
    int32_t *matches_left,
    bool first,
    int32_t skip_term,
    ecs_flags32_t iter_flags);

ecs_iter_t flecs_filter_iter_w_flags(
    const ecs_world_t *stage,
    const ecs_filter_t *filter,
    ecs_flags32_t flags);

void flecs_query_notify(
    ecs_world_t *world,
    ecs_query_t *query,
    ecs_query_event_t *event);

ecs_id_t flecs_to_public_id(
    ecs_id_t id);

ecs_id_t flecs_from_public_id(
    ecs_world_t *world,
    ecs_id_t id);

////////////////////////////////////////////////////////////////////////////////
//// Safe(r) integer casting
////////////////////////////////////////////////////////////////////////////////

#define FLECS_CONVERSION_ERR(T, value)\
    "illegal conversion from value " #value " to type " #T

#define flecs_signed_char__ (CHAR_MIN < 0)
#define flecs_signed_short__ true
#define flecs_signed_int__ true
#define flecs_signed_long__ true
#define flecs_signed_size_t__ false
#define flecs_signed_int8_t__ true
#define flecs_signed_int16_t__ true
#define flecs_signed_int32_t__ true
#define flecs_signed_int64_t__ true
#define flecs_signed_intptr_t__ true
#define flecs_signed_uint8_t__ false
#define flecs_signed_uint16_t__ false
#define flecs_signed_uint32_t__ false
#define flecs_signed_uint64_t__ false
#define flecs_signed_uintptr_t__ false
#define flecs_signed_ecs_size_t__ true
#define flecs_signed_ecs_entity_t__ false

uint64_t _flecs_ito(
    size_t dst_size,
    bool dst_signed,
    bool lt_zero,
    uint64_t value,
    const char *err);

#ifndef FLECS_NDEBUG
#define flecs_ito(T, value)\
    (T)_flecs_ito(\
        sizeof(T),\
        flecs_signed_##T##__,\
        (value) < 0,\
        (uint64_t)(value),\
        FLECS_CONVERSION_ERR(T, (value)))

#define flecs_uto(T, value)\
    (T)_flecs_ito(\
        sizeof(T),\
        flecs_signed_##T##__,\
        false,\
        (uint64_t)(value),\
        FLECS_CONVERSION_ERR(T, (value)))
#else
#define flecs_ito(T, value) (T)(value)
#define flecs_uto(T, value) (T)(value)
#endif

#define flecs_itosize(value) flecs_ito(size_t, (value))
#define flecs_utosize(value) flecs_uto(ecs_size_t, (value))
#define flecs_itoi16(value) flecs_ito(int16_t, (value))
#define flecs_itoi32(value) flecs_ito(int32_t, (value))


////////////////////////////////////////////////////////////////////////////////
//// Utilities
////////////////////////////////////////////////////////////////////////////////

uint64_t flecs_hash(
    const void *data,
    ecs_size_t length);

/* Get next power of 2 */
int32_t flecs_next_pow_of_2(
    int32_t n);

/* Convert 64bit value to ecs_record_t type. ecs_record_t is stored as 64bit int in the
 * entity index */
ecs_record_t flecs_to_row(
    uint64_t value);

/* Get 64bit integer from ecs_record_t */
uint64_t flecs_from_row(
    ecs_record_t record);

/* Get actual row from record row */
uint32_t flecs_record_to_row(
    uint32_t row, 
    bool *is_watched_out);

/* Convert actual row to record row */
uint32_t flecs_row_to_record(
    uint32_t row, 
    bool is_watched);

/* Convert a symbol name to an entity name by removing the prefix */
const char* flecs_name_from_symbol(
    ecs_world_t *world,
    const char *type_name);

/* Compare function for entity ids */
int flecs_entity_compare(
    ecs_entity_t e1, 
    const void *ptr1, 
    ecs_entity_t e2, 
    const void *ptr2); 

/* Compare function for entity ids which can be used with qsort */
int flecs_entity_compare_qsort(
    const void *e1,
    const void *e2);

/* Convert floating point to string */
char * ecs_ftoa(
    double f, 
    char * buf, 
    int precision);

uint64_t flecs_string_hash(
    const void *ptr);

void flecs_table_hashmap_init(
    ecs_hashmap_t *hm);

#define assert_func(cond) _assert_func(cond, #cond, __FILE__, __LINE__, __func__)
void _assert_func(
    bool cond,
    const char *cond_str,
    const char *file,
    int32_t line,
    const char *func);

#endif


/* Table sanity check to detect storage issues. Only enabled in SANITIZE mode as
 * this can severly slow down many ECS operations. */
#ifdef FLECS_SANITIZE
static
void check_table_sanity(ecs_table_t *table) {
    int32_t size = ecs_storage_size(&table->data.entities);
    int32_t count = ecs_storage_count(&table->data.entities);
    
    ecs_assert(size == ecs_storage_size(&table->data.records), 
        ECS_INTERNAL_ERROR, NULL);
    ecs_assert(count == ecs_storage_count(&table->data.records), 
        ECS_INTERNAL_ERROR, NULL);

    int32_t i;
    int32_t sw_offset = table->sw_offset;
    int32_t sw_count = table->sw_count;
    int32_t bs_offset = table->bs_offset;
    int32_t bs_count = table->bs_count;
    int32_t type_count = table->type.count;
    ecs_id_t *ids = table->type.array;

    ecs_assert((sw_count + sw_offset) <= type_count, ECS_INTERNAL_ERROR, NULL);
    ecs_assert((bs_count + bs_offset) <= type_count, ECS_INTERNAL_ERROR, NULL);

    ecs_table_t *storage_table = table->storage_table;
    if (storage_table) {
        ecs_assert(table->storage_count == storage_table->type.count,
            ECS_INTERNAL_ERROR, NULL);
        ecs_assert(table->storage_ids == storage_table->type.array, 
            ECS_INTERNAL_ERROR, NULL);

        int32_t storage_count = table->storage_count;
        ecs_assert(type_count >= storage_count, ECS_INTERNAL_ERROR, NULL);

        int32_t *storage_map = table->storage_map;
        ecs_assert(storage_map != NULL, ECS_INTERNAL_ERROR, NULL);

        ecs_id_t *storage_ids = table->storage_ids;
        for (i = 0; i < type_count; i ++) {
            if (storage_map[i] != -1) {
                ecs_assert(ids[i] == storage_ids[storage_map[i]],
                    ECS_INTERNAL_ERROR, NULL);
            }
        }

        ecs_assert(table->data.columns != NULL, ECS_INTERNAL_ERROR, NULL);
        ecs_assert(table->type_info != NULL, ECS_INTERNAL_ERROR, NULL);

        for (i = 0; i < storage_count; i ++) {
            ecs_column_t *column = &table->data.columns[i];
            ecs_assert(size == column->size, ECS_INTERNAL_ERROR, NULL);
            ecs_assert(count == column->count, ECS_INTERNAL_ERROR, NULL);
            int32_t storage_map_id = storage_map[i + type_count];
            ecs_assert(storage_map_id >= 0, ECS_INTERNAL_ERROR, NULL);
            ecs_assert(ids[storage_map_id] == storage_ids[i],
                ECS_INTERNAL_ERROR, NULL);
        }
    } else {
        ecs_assert(table->storage_count == 0, ECS_INTERNAL_ERROR, NULL);
        ecs_assert(table->storage_ids == NULL, ECS_INTERNAL_ERROR, NULL);
    }

    if (sw_count) {
        ecs_assert(table->data.sw_columns != NULL, 
            ECS_INTERNAL_ERROR, NULL);
        for (i = 0; i < sw_count; i ++) {
            ecs_switch_t *sw = &table->data.sw_columns[i];
            ecs_assert(ecs_vector_count(sw->values) == count, 
                ECS_INTERNAL_ERROR, NULL);
            ecs_assert(ECS_PAIR_FIRST(ids[i + sw_offset]) == EcsUnion,
                ECS_INTERNAL_ERROR, NULL);
        }
    }

    if (bs_count) {
        ecs_assert(table->data.bs_columns != NULL, 
            ECS_INTERNAL_ERROR, NULL);
        for (i = 0; i < bs_count; i ++) {
            ecs_bitset_t *bs = &table->data.bs_columns[i];
            ecs_assert(flecs_bitset_count(bs) == count,
                ECS_INTERNAL_ERROR, NULL);
            ecs_assert((ids[i + bs_offset] & ECS_ROLE_MASK) == 
                    ECS_DISABLED, ECS_INTERNAL_ERROR, NULL);
        }
    }
}
#else
#define check_table_sanity(table)
#endif

static
void flecs_table_init_storage_map(
    ecs_table_t *table)
{
    ecs_assert(table != NULL, ECS_INTERNAL_ERROR, NULL);
    if (!table->storage_table) {
        return;
    }

    ecs_type_t type = table->type;
    ecs_id_t *ids = type.array;
    int32_t t, ids_count = type.count;
    ecs_id_t *storage_ids = table->storage_ids;
    int32_t s, storage_ids_count = table->storage_count;

    if (!ids_count) {
        table->storage_map = NULL;
        return;
    }

    table->storage_map = ecs_os_malloc_n(
        int32_t, ids_count + storage_ids_count);

    int32_t *t2s = table->storage_map;
    int32_t *s2t = &table->storage_map[ids_count];

    for (s = 0, t = 0; (t < ids_count) && (s < storage_ids_count); ) {
        ecs_id_t id = ids[t];
        ecs_id_t storage_id = storage_ids[s];

        if (id == storage_id) {
            t2s[t] = s;
            s2t[s] = t;
        } else {
            t2s[t] = -1;
        }

        /* Ids can never get ahead of storage id, as ids are a superset of the
         * storage ids */
        ecs_assert(id <= storage_id, ECS_INTERNAL_ERROR, NULL);

        t += (id <= storage_id);
        s += (id == storage_id);
    }

    /* Storage ids is always a subset of ids, so all should be iterated */
    ecs_assert(s == storage_ids_count, ECS_INTERNAL_ERROR, NULL);

    /* Initialize remainder of type -> storage_type map */
    for (; (t < ids_count); t ++) {
        t2s[t] = -1;
    }
}

static
ecs_flags32_t flecs_type_info_flags(
    const ecs_type_info_t *ti) 
{
    ecs_flags32_t flags = 0;

    if (ti->hooks.ctor) {
        flags |= EcsTableHasCtors;
    }
    if (ti->hooks.on_add) {
        flags |= EcsTableHasCtors;
    }
    if (ti->hooks.dtor) {
        flags |= EcsTableHasDtors;
    }
    if (ti->hooks.on_remove) {
        flags |= EcsTableHasDtors;
    }
    if (ti->hooks.copy) {
        flags |= EcsTableHasCopy;
    }
    if (ti->hooks.move) {
        flags |= EcsTableHasMove;
    }  

    return flags;  
}

static
void flecs_table_init_type_info(
    ecs_table_t *table)
{
    ecs_assert(table != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(table->storage_table == table, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(table->type_info == NULL, ECS_INTERNAL_ERROR, NULL);

    ecs_table_record_t *records = table->records;
    int32_t i, count = table->type.count;
    table->type_info = ecs_os_calloc_n(ecs_type_info_t*, count);

    for (i = 0; i < count; i ++) {
        ecs_table_record_t *tr = &records[i];
        ecs_id_record_t *idr = (ecs_id_record_t*)tr->hdr.cache;
        
        /* All ids in the storage table must be components with type info */
        const ecs_type_info_t *ti = idr->type_info;
        ecs_assert(ti != NULL, ECS_INTERNAL_ERROR, NULL);
        table->flags |= flecs_type_info_flags(ti);
        table->type_info[i] = (ecs_type_info_t*)ti;
    }
}

static
void flecs_table_init_storage_table(
    ecs_world_t *world,
    ecs_table_t *table)
{
    if (table->storage_table) {
        return;
    }

    ecs_type_t type = table->type;
    int32_t i, count = type.count;
    ecs_id_t *ids = type.array;
    ecs_table_record_t *records = table->records;

    ecs_id_t array[ECS_ID_CACHE_SIZE];
    ecs_type_t storage_ids = { .array = array };
    if (count > ECS_ID_CACHE_SIZE) {
        storage_ids.array = ecs_os_malloc_n(ecs_id_t, count);
    }

    for (i = 0; i < count; i ++) {
        ecs_table_record_t *tr = &records[i];
        ecs_id_record_t *idr = (ecs_id_record_t*)tr->hdr.cache;
        ecs_id_t id = ids[i];

        if (idr->type_info != NULL) {
            storage_ids.array[storage_ids.count ++] = id;
        }
    }

    if (storage_ids.count && storage_ids.count != count) {
        ecs_table_t *storage_table = flecs_table_find_or_create(world, 
            &storage_ids);
        table->storage_table = storage_table;
        table->storage_count = flecs_ito(uint16_t, storage_ids.count);
        table->storage_ids = storage_table->type.array;
        table->type_info = storage_table->type_info;
        table->flags |= storage_table->flags;
        storage_table->refcount ++;
    } else if (storage_ids.count) {
        table->storage_table = table;
        table->storage_count = flecs_ito(uint16_t, count);
        table->storage_ids = type.array;
        flecs_table_init_type_info(table);
    }

    if (storage_ids.array != array) {
        ecs_os_free(storage_ids.array);
    }

    if (!table->storage_map) {
        flecs_table_init_storage_map(table);
    }
}

void flecs_table_init_data(
    ecs_table_t *table)
{
    int32_t sw_count = table->sw_count;
    int32_t bs_count = table->bs_count;

    ecs_data_t *storage = &table->data;
    int32_t i, count = table->storage_count;

    /* Root tables don't have columns */
    if (!count && !sw_count && !bs_count) {
        storage->columns = NULL;
    }

    if (count) {
        storage->columns = ecs_os_calloc_n(ecs_column_t, count);
    }

    if (sw_count) {
        storage->sw_columns = ecs_os_calloc_n(ecs_switch_t, sw_count);
        for (i = 0; i < sw_count; i ++) {
            flecs_switch_init(&storage->sw_columns[i], 0);
        }
    }

    if (bs_count) {
        storage->bs_columns = ecs_os_calloc_n(ecs_bitset_t, bs_count);
        for (i = 0; i < bs_count; i ++) {
            flecs_bitset_init(&storage->bs_columns[i]);
        }
    }
}

static
void flecs_table_init_flags(
    ecs_world_t *world,
    ecs_table_t *table)
{
    ecs_id_t *ids = table->type.array;
    int32_t count = table->type.count;

    /* Iterate components to initialize table flags */
    int32_t i;
    for (i = 0; i < count; i ++) {
        ecs_id_t id = ids[i];

        /* As we're iterating over the table components, also set the table
         * flags. These allow us to quickly determine if the table contains
         * data that needs to be handled in a special way. */

        if (id <= EcsLastInternalComponentId) {
            table->flags |= EcsTableHasBuiltins;
        }

        if (id == EcsModule) {
            table->flags |= EcsTableHasBuiltins;
            table->flags |= EcsTableHasModule;
        } else if (id == EcsPrefab) {
            table->flags |= EcsTableIsPrefab;
        } else if (id == EcsDisabled) {
            table->flags |= EcsTableIsDisabled;
        } else {
            ecs_entity_t role = id & ECS_ROLE_MASK;
            if (role == ECS_PAIR) {
                ecs_entity_t r = ECS_PAIR_FIRST(id);

                table->flags |= EcsTableHasPairs;

                if (r == EcsIsA) {
                    table->flags |= EcsTableHasIsA;
                } else if (r == EcsChildOf) {
                    table->flags |= EcsTableHasChildOf;
                    ecs_entity_t obj = ecs_pair_second(world, id);
                    ecs_assert(obj != 0, ECS_INTERNAL_ERROR, NULL);

                    if (obj == EcsFlecs || obj == EcsFlecsCore || 
                        ecs_has_id(world, obj, EcsModule)) 
                    {
                        /* If table contains entities that are inside one of the 
                         * builtin modules, it contains builtin entities */
                        table->flags |= EcsTableHasBuiltins;
                        table->flags |= EcsTableHasModule;
                    }
                } else if (r == EcsUnion) {
                    table->flags |= EcsTableHasUnion;

                    if (!table->sw_count) {
                        table->sw_offset = flecs_ito(int16_t, i);
                    }
                    table->sw_count ++;
                } else if (r == ecs_id(EcsPoly)) {
                    table->flags |= EcsTableHasBuiltins;
                }
            } else if (role == ECS_DISABLED) {
                table->flags |= EcsTableHasDisabled;

                if (!table->bs_count) {
                    table->bs_offset = flecs_ito(int16_t, i);
                }
                table->bs_count ++;
            } else if (role == ECS_OVERRIDE) {
                table->flags |= EcsTableHasOverrides;
            }
        } 
    }
}

void flecs_table_init(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_table_t *from)
{
    flecs_table_init_flags(world, table);

    int32_t dst_i = 0, dst_count = table->type.count;
    int32_t src_i = 0, src_count = 0;
    ecs_id_t *dst_ids = table->type.array;
    ecs_id_t *src_ids = NULL;
    ecs_table_record_t *tr = NULL, *src_tr = NULL;
    if (from) {
        src_count = from->type.count;
        src_ids = from->type.array;
        src_tr = from->records;
    }

    /* We don't know in advance how large the records array will be, so use
     * cached vector. This eliminates unnecessary allocations, and/or expensive
     * iterations to determine how many records we need. */
    ecs_vector_t *records = world->store.records;
    ecs_vector_clear(records);
    ecs_id_record_t *idr;

    int32_t last_id = -1; /* Track last regular (non-pair) id */
    int32_t first_pair = -1; /* Track the first pair in the table */
    int32_t first_role = -1; /* Track first id with role */

    /* Scan to find boundaries of regular ids, pairs and roles */
    for (dst_i = 0; dst_i < dst_count; dst_i ++) {
        ecs_id_t dst_id = dst_ids[dst_i];
        ecs_entity_t role = dst_id & ECS_ROLE_MASK;
        if (first_pair == -1 && ECS_HAS_ROLE(dst_id, PAIR)) {
            first_pair = dst_i;
        }
        if ((dst_id & ECS_COMPONENT_MASK) == dst_id) {
            last_id = dst_i;
        } else if (first_role == -1 && role != ECS_PAIR) {
            first_role = dst_i;
        }
    }

    /* The easy part: initialize a record for every id in the type */
    for (dst_i = 0; (dst_i < dst_count) && (src_i < src_count); ) {
        ecs_id_t dst_id = dst_ids[dst_i];
        ecs_id_t src_id = src_ids[src_i];

        idr = NULL;

        if (dst_id == src_id) {
            idr = (ecs_id_record_t*)src_tr[src_i].hdr.cache;
        } else if (dst_id < src_id) {
            idr = flecs_id_record_ensure(world, dst_id);
        }
        if (idr) {
            tr = ecs_vector_add(&records, ecs_table_record_t);
            tr->hdr.cache = (ecs_table_cache_t*)idr;
            tr->column = dst_i;
            tr->count = 1;
        }

        dst_i += dst_id <= src_id;
        src_i += dst_id >= src_id;
    }

    /* Add remaining ids that the "from" table didn't have */
    for (; (dst_i < dst_count); dst_i ++) {
        ecs_id_t dst_id = dst_ids[dst_i];
        tr = ecs_vector_add(&records, ecs_table_record_t);
        idr = flecs_id_record_ensure(world, dst_id);
        tr->hdr.cache = (ecs_table_cache_t*)idr;
        ecs_assert(tr->hdr.cache != NULL, ECS_INTERNAL_ERROR, NULL);
        tr->column = dst_i;
        tr->count = 1;
    }

    /* Add records for ids with roles (used by cleanup logic) */
    if (first_role != -1) {
        for (dst_i = first_role; dst_i < dst_count; dst_i ++) {
            ecs_id_t id = dst_ids[dst_i];
            ecs_entity_t role = id & ECS_ROLE_MASK;
            if (role != ECS_PAIR) {
                id &= ECS_COMPONENT_MASK;
                id = ecs_pair(id, EcsWildcard);
                tr = ecs_vector_add(&records, ecs_table_record_t);
                tr->hdr.cache = (ecs_table_cache_t*)flecs_id_record_ensure(
                    world, id);
                tr->column = dst_i;
                tr->count = 1;
            }
        }
    }

    int32_t last_pair = -1;
    int32_t first_tgt_wc = -1;
    int32_t tgt_wc_count = 0;
    bool has_childof = table->flags & EcsTableHasChildOf;
    if (first_pair != -1) {
        /* Add a (Relationship, *) record for each relationship. */
        ecs_entity_t r = 0;
        for (dst_i = first_pair; dst_i < dst_count; dst_i ++) {
            ecs_id_t dst_id = dst_ids[dst_i];
            if (!ECS_HAS_ROLE(dst_id, PAIR)) {
                break; /* no more pairs */
            }
            if (r != ECS_PAIR_FIRST(dst_id)) { /* New relationship, new record */
                tr = ecs_vector_get(records, ecs_table_record_t, dst_i);
                idr = ((ecs_id_record_t*)tr->hdr.cache)->parent; /* (R, *) */
                ecs_assert(idr != NULL, ECS_INTERNAL_ERROR, NULL);

                tr = ecs_vector_add(&records, ecs_table_record_t);
                tr->hdr.cache = (ecs_table_cache_t*)idr;
                tr->column = dst_i;
                tr->count = 0;
                r = ECS_PAIR_FIRST(dst_id);
            }

            ecs_assert(tr != NULL, ECS_INTERNAL_ERROR, NULL);
            tr->count ++;
        }

        last_pair = dst_i;

        /* Add a (*, Target) record for each relationship target. Type
         * ids are sorted relationship-first, so we can't simply do a single linear 
         * scan to find all occurrences for a target. */

        /* We're going to insert records from the vector into the index that
         * will get patched up later. To ensure the record pointers don't get
         * invalidated we need to grow the vector so that it won't realloc as
         * we're adding the next set of records */

        int wildcard_count = 3; /* for *, _ and (*, *) */
        wildcard_count += dst_count && !has_childof; /* for (ChildOf, 0) */
        
        ecs_vector_set_min_size(&records, ecs_table_record_t,   
            ecs_vector_count(records) + wildcard_count +
                (last_pair - first_pair));

        for (dst_i = first_pair; dst_i < last_pair; dst_i ++) {
            ecs_id_t dst_id = dst_ids[dst_i];
            ecs_id_t tgt_id = ecs_pair(EcsWildcard, ECS_PAIR_SECOND(dst_id));

            /* To avoid a quadratic search, use the O(1) lookup that the index
             * already provides. */
            idr = flecs_id_record_ensure(world, tgt_id);
            tr = (ecs_table_record_t*)flecs_id_record_get_table(idr, table);
            if (!tr) {
                tr = ecs_vector_add(&records, ecs_table_record_t);
                tr->column = dst_i;
                tr->count = 1;

                ecs_table_cache_insert(&idr->cache, table, &tr->hdr);

                if (first_tgt_wc == -1) {
                    first_tgt_wc = ecs_vector_count(records) - 1;
                }

                tgt_wc_count ++;
            } else {
                tr->count ++;
            }

            ecs_assert(tr->hdr.cache != NULL, ECS_INTERNAL_ERROR, NULL);
        }
    }

    /* Lastly, add records for all-wildcard ids */
    if (last_id >= 0) {
        tr = ecs_vector_add(&records, ecs_table_record_t);
        tr->hdr.cache = (ecs_table_cache_t*)world->idr_wildcard;
        tr->column = 0;
        tr->count = last_id + 1;
    }
    if (last_pair - first_pair) {
        tr = ecs_vector_add(&records, ecs_table_record_t);
        tr->hdr.cache = (ecs_table_cache_t*)world->idr_wildcard_wildcard;
        tr->column = first_pair;
        tr->count = last_pair - first_pair;
    }
    if (dst_count) {
        tr = ecs_vector_add(&records, ecs_table_record_t);
        tr->hdr.cache = (ecs_table_cache_t*)world->idr_any;
        tr->column = 0;
        tr->count = 1;
    }
    if (dst_count && !has_childof) {
        tr = ecs_vector_add(&records, ecs_table_record_t);
        tr->hdr.cache = (ecs_table_cache_t*)world->idr_childof_0;
        tr->column = 0;
        tr->count = 1;
    }

    /* Now that all records have been added, copy them to array */
    int32_t i, dst_record_count = ecs_vector_count(records);
    ecs_table_record_t *dst_tr =  ecs_os_memdup_n( ecs_vector_first(records, 
        ecs_table_record_t), ecs_table_record_t, dst_record_count);
    table->record_count = flecs_ito(uint16_t, dst_record_count);
    table->records = dst_tr;

    /* Register & patch up records */
    for (i = 0; i < dst_record_count; i ++) {
        tr = &dst_tr[i];
        idr = (ecs_id_record_t*)dst_tr[i].hdr.cache;
        ecs_assert(idr != NULL, ECS_INTERNAL_ERROR, NULL);

        if (i >= first_tgt_wc && i < (first_tgt_wc + tgt_wc_count)) {
            /* If this is a target wildcard record it has already been 
             * registered, but the record is now at a different location in
             * memory. Patch up the linked list with the new address */
            ecs_table_cache_replace(&idr->cache, table, &tr->hdr);
        } else {
            /* Other records are not registered yet */
            ecs_assert(idr != NULL, ECS_INTERNAL_ERROR, NULL);
            ecs_table_cache_insert(&idr->cache, table, &tr->hdr);
        }

        /* Claim id record so it stays alive as long as the table exists */
        flecs_id_record_claim(world, idr);

        /* Initialize event flags */
        table->flags |= idr->flags & EcsIdEventMask;
    }

    world->store.records = records;

    flecs_table_init_storage_table(world, table);
    flecs_table_init_data(table); 
}

static
void flecs_table_records_unregister(
    ecs_world_t *world,
    ecs_table_t *table)
{
    int32_t i, count = table->record_count;
    for (i = 0; i < count; i ++) {
        ecs_table_record_t *tr = &table->records[i];
        ecs_table_cache_t *cache = tr->hdr.cache;
        ecs_id_t id = ((ecs_id_record_t*)cache)->id;

        ecs_assert(tr->hdr.cache == cache, ECS_INTERNAL_ERROR, NULL);
        ecs_assert(tr->hdr.table == table, ECS_INTERNAL_ERROR, NULL);
        ecs_assert(flecs_id_record_get(world, id) == (ecs_id_record_t*)cache,
            ECS_INTERNAL_ERROR, NULL);
        (void)id;

        ecs_table_cache_remove(cache, table, &tr->hdr);
        flecs_id_record_release(world, (ecs_id_record_t*)cache);
    }
    
    ecs_os_free(table->records);
}

static
void notify_trigger(
    ecs_world_t *world, 
    ecs_table_t *table, 
    ecs_entity_t event) 
{
    (void)world;

    if (event == EcsOnAdd) {
        table->flags |= EcsTableHasOnAdd;
    } else if (event == EcsOnRemove) {
        table->flags |= EcsTableHasOnRemove;
    } else if (event == EcsOnSet) {
        table->flags |= EcsTableHasOnSet;
    } else if (event == EcsUnSet) {
        table->flags |= EcsTableHasUnSet;
    }
}

static
void run_on_remove(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_data_t *data)
{
    int32_t count = data->entities.count;
    if (count) {
        ecs_table_diff_t diff = {
            .removed = table->type,
            .un_set = table->type
        };
        
        flecs_notify_on_remove(world, table, NULL, 0, count, &diff);
    }
}

/* -- Private functions -- */

static
void on_component_callback(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_iter_action_t callback,
    ecs_entity_t event,
    ecs_column_t *column,
    ecs_entity_t *entities,
    ecs_id_t id,
    int32_t row,
    int32_t count,
    ecs_type_info_t *ti)
{
    ecs_assert(ti != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_iter_t it = { .term_count = 1 };
    it.entities = entities;

    ecs_size_t size = ti->size;
    void *ptr = ecs_storage_get(column, size, row);

    flecs_iter_init(&it, flecs_iter_cache_all);
    it.world = world;
    it.real_world = world;
    it.table = table;
    it.ptrs[0] = ptr;
    it.sizes[0] = size;
    it.ids[0] = id;
    it.event = event;
    it.event_id = id;
    it.ctx = ti->hooks.ctx;
    it.binding_ctx = ti->hooks.binding_ctx;
    it.count = count;
    flecs_iter_validate(&it);
    callback(&it);
}

static
void ctor_component(
    ecs_type_info_t *ti,
    ecs_column_t *column,
    int32_t row,
    int32_t count)
{
    ecs_assert(ti != NULL, ECS_INTERNAL_ERROR, NULL);

    ecs_xtor_t ctor = ti->hooks.ctor;
    if (ctor) {
        void *ptr = ecs_storage_get(column, ti->size, row);
        ctor(ptr, count, ti);
    }
}

static
void add_component(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_type_info_t *ti,
    ecs_column_t *column,
    ecs_entity_t *entities,
    ecs_id_t id,
    int32_t row,
    int32_t count)
{
    ecs_assert(ti != NULL, ECS_INTERNAL_ERROR, NULL);

    ctor_component(ti, column, row, count);

    ecs_iter_action_t on_add = ti->hooks.on_add;
    if (on_add) {
        on_component_callback(world, table, on_add, EcsOnAdd, column,
            entities, id, row, count, ti);
    }
}

static
void dtor_component(
    ecs_type_info_t *ti,
    ecs_column_t *column,
    int32_t row,
    int32_t count)
{
    ecs_assert(ti != NULL, ECS_INTERNAL_ERROR, NULL);

    ecs_xtor_t dtor = ti->hooks.dtor;
    if (dtor) {
        void *ptr = ecs_storage_get(column, ti->size, row);
        dtor(ptr, count, ti);
    }
}

static
void remove_component(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_type_info_t *ti,
    ecs_column_t *column,
    ecs_entity_t *entities,
    ecs_id_t id,
    int32_t row,
    int32_t count)
{
    ecs_assert(ti != NULL, ECS_INTERNAL_ERROR, NULL);

    ecs_iter_action_t on_remove = ti->hooks.on_remove;
    if (on_remove) {
        on_component_callback(world, table, on_remove, EcsOnRemove, column,
            entities, id, row, count, ti);
    }
    
    dtor_component(ti, column, row, count);
}

static
void dtor_all_components(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_data_t *data,
    int32_t row,
    int32_t count,
    bool update_entity_index,
    bool is_delete)
{
    /* Can't delete and not update the entity index */
    ecs_assert(!is_delete || update_entity_index, ECS_INTERNAL_ERROR, NULL);

    ecs_id_t *ids = table->storage_ids;
    int32_t ids_count = table->storage_count;
    ecs_record_t **records = data->records.array;
    ecs_entity_t *entities = data->entities.array;
    int32_t i, c, end = row + count;

    (void)records;

    /* If table has components with destructors, iterate component columns */
    if (table->flags & EcsTableHasDtors) {
        /* Throw up a lock just to be sure */
        table->lock = true;

        /* Run on_remove callbacks first before destructing components */
        for (c = 0; c < ids_count; c++) {
            ecs_column_t *column = &data->columns[c];
            ecs_type_info_t *ti = table->type_info[c];
            ecs_iter_action_t on_remove = ti->hooks.on_remove;
            if (on_remove) {
                on_component_callback(world, table, on_remove, EcsOnRemove, 
                    column, &entities[row], ids[c], row, count, ti);
            }
        }

        /* Destruct components */
        for (c = 0; c < ids_count; c++) {
            dtor_component(table->type_info[c], &data->columns[c], row, count);
        }

        /* Iterate entities first, then components. This ensures that only one
         * entity is invalidated at a time, which ensures that destructors can
         * safely access other entities. */
        for (i = row; i < end; i ++) {
            /* Update entity index after invoking destructors so that entity can
             * be safely used in destructor callbacks. */
            if (update_entity_index) {
                ecs_entity_t e = entities[i];
                ecs_assert(!e || ecs_is_valid(world, e), 
                    ECS_INTERNAL_ERROR, NULL);
                ecs_assert(!e || records[i] == flecs_entities_get(world, e), 
                    ECS_INTERNAL_ERROR, NULL);
                ecs_assert(!e || records[i]->table == table, 
                    ECS_INTERNAL_ERROR, NULL);

                if (is_delete) {
                    flecs_entities_remove(world, e);
                    ecs_assert(ecs_is_valid(world, e) == false, 
                        ECS_INTERNAL_ERROR, NULL);
                } else {
                    // If this is not a delete, clear the entity index record
                    records[i]->table = NULL;
                    records[i]->row = 0;
                }
            } else {
                /* This should only happen in rare cases, such as when the data
                 * cleaned up is not part of the world (like with snapshots) */
            }
        }

        table->lock = false;

    /* If table does not have destructors, just update entity index */
    } else if (update_entity_index) {
        if (is_delete) {
            for (i = row; i < end; i ++) {
                ecs_entity_t e = entities[i];
                ecs_assert(!e || ecs_is_valid(world, e), ECS_INTERNAL_ERROR, NULL);
                ecs_assert(!e || records[i] == flecs_entities_get(world, e), 
                    ECS_INTERNAL_ERROR, NULL);
                ecs_assert(!e || records[i]->table == table, 
                    ECS_INTERNAL_ERROR, NULL);

                flecs_entities_remove(world, e);
                ecs_assert(!ecs_is_valid(world, e), ECS_INTERNAL_ERROR, NULL);
            } 
        } else {
            for (i = row; i < end; i ++) {
                ecs_entity_t e = entities[i];
                ecs_assert(!e || ecs_is_valid(world, e), ECS_INTERNAL_ERROR, NULL);
                ecs_assert(!e || records[i] == flecs_entities_get(world, e), 
                    ECS_INTERNAL_ERROR, NULL);
                ecs_assert(!e || records[i]->table == table, 
                    ECS_INTERNAL_ERROR, NULL);
                records[i]->table = NULL;
                records[i]->row = records[i]->row & ECS_ROW_FLAGS_MASK;
                (void)e;
            }
        }      
    }
}

static
void fini_data(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_data_t *data,
    bool do_on_remove,
    bool update_entity_index,
    bool is_delete,
    bool deactivate)
{
    ecs_assert(!table->lock, ECS_LOCKED_STORAGE, NULL);

    if (!data) {
        return;
    }

    ecs_flags32_t flags = table->flags;

    if (do_on_remove && (flags & EcsTableHasOnRemove)) {
        run_on_remove(world, table, data);        
    }

    int32_t count = flecs_table_data_count(data);
    if (count) {
        dtor_all_components(world, table, data, 0, count, 
            update_entity_index, is_delete);
    }

    /* Sanity check */
    ecs_assert(data->records.count == 
        data->entities.count, ECS_INTERNAL_ERROR, NULL);

    ecs_column_t *columns = data->columns;
    if (columns) {
        int32_t c, column_count = table->storage_count;
        for (c = 0; c < column_count; c ++) {
            /* Sanity check */
            ecs_assert(columns[c].count == data->entities.count,
                ECS_INTERNAL_ERROR, NULL);

            ecs_storage_fini(&columns[c]);
        }
        ecs_os_free(columns);
        data->columns = NULL;
    }

    ecs_switch_t *sw_columns = data->sw_columns;
    if (sw_columns) {
        int32_t c, column_count = table->sw_count;
        for (c = 0; c < column_count; c ++) {
            flecs_switch_fini(&sw_columns[c]);
        }
        ecs_os_free(sw_columns);
        data->sw_columns = NULL;
    }

    ecs_bitset_t *bs_columns = data->bs_columns;
    if (bs_columns) {
        int32_t c, column_count = table->bs_count;
        for (c = 0; c < column_count; c ++) {
            flecs_bitset_fini(&bs_columns[c]);
        }
        ecs_os_free(bs_columns);
        data->bs_columns = NULL;
    }

    ecs_storage_fini(&data->entities);
    ecs_storage_fini(&data->records);

    if (deactivate && count) {
        flecs_table_set_empty(world, table);
    }
}

/* Cleanup, no OnRemove, don't update entity index, don't deactivate table */
void flecs_table_clear_data(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_data_t *data)
{
    fini_data(world, table, data, false, false, false, false);
}

/* Cleanup, no OnRemove, clear entity index, deactivate table */
void flecs_table_clear_entities_silent(
    ecs_world_t *world,
    ecs_table_t *table)
{
    fini_data(world, table, &table->data, false, true, false, true);
}

/* Cleanup, run OnRemove, clear entity index, deactivate table */
void flecs_table_clear_entities(
    ecs_world_t *world,
    ecs_table_t *table)
{
    fini_data(world, table, &table->data, true, true, false, true);
}

/* Cleanup, run OnRemove, delete from entity index, deactivate table */
void flecs_table_delete_entities(
    ecs_world_t *world,
    ecs_table_t *table)
{
    fini_data(world, table, &table->data, true, true, true, true);
}

/* Unset all components in table. This function is called before a table is 
 * deleted, and invokes all UnSet handlers, if any */
void flecs_table_remove_actions(
    ecs_world_t *world,
    ecs_table_t *table)
{
    (void)world;
    run_on_remove(world, table, &table->data);
}

/* Free table resources. */
void flecs_table_free(
    ecs_world_t *world,
    ecs_table_t *table)
{
    bool is_root = table == &world->store.root;
    ecs_assert(!table->lock, ECS_LOCKED_STORAGE, NULL);
    ecs_assert(is_root || table->id != 0, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(is_root || flecs_sparse_is_alive(&world->store.tables, table->id),
        ECS_INTERNAL_ERROR, NULL);
    (void)world;

    ecs_assert(table->refcount == 0, ECS_INTERNAL_ERROR, NULL);

    if (!is_root) {
        flecs_notify_queries(
            world, &(ecs_query_event_t){
                .kind = EcsQueryTableUnmatch,
                .table = table
            });
    }

    if (ecs_should_log_2()) {
        char *expr = ecs_type_str(world, &table->type);
        ecs_dbg_2(
            "#[green]table#[normal] [%s] #[red]deleted#[reset] with id %d", 
            expr, table->id);
        ecs_os_free(expr);
        ecs_log_push_2();
    }

    world->info.empty_table_count -= (ecs_table_count(table) == 0);

    /* Cleanup data, no OnRemove, delete from entity index, don't deactivate */
    fini_data(world, table, &table->data, false, true, true, false);

    flecs_table_clear_edges(world, table);

    if (!is_root) {
        ecs_type_t ids = {
            .array = table->type.array,
            .count = table->type.count
        };

        flecs_hashmap_remove(&world->store.table_map, &ids, ecs_table_t*);
    }

    ecs_os_free(table->dirty_state);
    ecs_os_free(table->storage_map);

    flecs_table_records_unregister(world, table);

    ecs_table_t *storage_table = table->storage_table;
    if (storage_table == table) {
        if (table->type_info) {
            ecs_os_free(table->type_info);
        }
    } else if (storage_table) {
        flecs_table_release(world, storage_table);
    }

    /* Update counters */
    world->info.table_count --;
    world->info.table_record_count -= table->record_count;
    world->info.table_storage_count -= table->storage_count;
    world->info.table_delete_total ++;

    if (!table->storage_count) {
        world->info.tag_table_count --;
    } else {
        world->info.trivial_table_count -= !(table->flags & EcsTableIsComplex);
    }

    if (!(world->flags & EcsWorldFini)) {
        ecs_assert(!is_root, ECS_INTERNAL_ERROR, NULL);
        flecs_table_free_type(table);
        flecs_sparse_remove(&world->store.tables, table->id);
    }

    ecs_log_pop_2();
}

void flecs_table_claim(
    ecs_world_t *world,
    ecs_table_t *table)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_assert(table != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(table->refcount > 0, ECS_INTERNAL_ERROR, NULL);
    table->refcount ++;
    (void)world;
}

bool flecs_table_release(
    ecs_world_t *world,
    ecs_table_t *table)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_assert(table != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(table->refcount > 0, ECS_INTERNAL_ERROR, NULL);

    if (--table->refcount == 0) {
        flecs_table_free(world, table);
        return true;
    }
    
    return false;
}

/* Free table type. Do this separately from freeing the table as types can be
 * in use by application destructors. */
void flecs_table_free_type(
    ecs_table_t *table)
{
    ecs_os_free(table->type.array);
}

/* Reset a table to its initial state. */
void flecs_table_reset(
    ecs_world_t *world,
    ecs_table_t *table)
{
    ecs_assert(!table->lock, ECS_LOCKED_STORAGE, NULL);
    flecs_table_clear_edges(world, table);
}

static
void mark_table_dirty(
    ecs_world_t *world,
    ecs_table_t *table,
    int32_t index)
{
    (void)world;
    if (table->dirty_state) {
        table->dirty_state[index] ++;
    }
}

void flecs_table_mark_dirty(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_entity_t component)
{
    ecs_assert(!table->lock, ECS_LOCKED_STORAGE, NULL);
    ecs_assert(table != NULL, ECS_INTERNAL_ERROR, NULL);

    if (table->dirty_state) {
        int32_t index = ecs_search(world, table->storage_table, component, 0);
        ecs_assert(index != -1, ECS_INTERNAL_ERROR, NULL);
        table->dirty_state[index + 1] ++;
    }
}

static
void move_switch_columns(
    ecs_table_t *dst_table, 
    int32_t dst_index,
    ecs_table_t *src_table, 
    int32_t src_index,
    int32_t count,
    bool clear)
{
    int32_t i_old = 0, src_column_count = src_table->sw_count;
    int32_t i_new = 0, dst_column_count = dst_table->sw_count;

    if (!src_column_count && !dst_column_count) {
        return;
    }

    ecs_switch_t *src_columns = src_table->data.sw_columns;
    ecs_switch_t *dst_columns = dst_table->data.sw_columns;

    ecs_type_t dst_type = dst_table->type;
    ecs_type_t src_type = src_table->type;

    int32_t offset_new = dst_table->sw_offset;
    int32_t offset_old = src_table->sw_offset;

    ecs_id_t *dst_ids = dst_type.array;
    ecs_id_t *src_ids = src_type.array;

    for (; (i_new < dst_column_count) && (i_old < src_column_count);) {
        ecs_entity_t dst_id = dst_ids[i_new + offset_new];
        ecs_entity_t src_id = src_ids[i_old + offset_old];

        if (dst_id == src_id) {
            ecs_switch_t *src_switch = &src_columns[i_old];
            ecs_switch_t *dst_switch = &dst_columns[i_new];

            flecs_switch_ensure(dst_switch, dst_index + count);

            int i;
            for (i = 0; i < count; i ++) {
                uint64_t value = flecs_switch_get(src_switch, src_index + i);
                flecs_switch_set(dst_switch, dst_index + i, value);
            }

            if (clear) {
                ecs_assert(count == flecs_switch_count(src_switch), 
                    ECS_INTERNAL_ERROR, NULL);
                flecs_switch_clear(src_switch);
            }
        } else if (dst_id > src_id) {
            ecs_switch_t *src_switch = &src_columns[i_old];
            flecs_switch_clear(src_switch);
        }

        i_new += dst_id <= src_id;
        i_old += dst_id >= src_id;
    }

    /* Clear remaining columns */
    if (clear) {
        for (; (i_old < src_column_count); i_old ++) {
            ecs_switch_t *src_switch = &src_columns[i_old];
            ecs_assert(count == flecs_switch_count(src_switch), 
                ECS_INTERNAL_ERROR, NULL);
            flecs_switch_clear(src_switch);
        }
    }
}

static
void move_bitset_columns(
    ecs_table_t *dst_table, 
    int32_t dst_index,
    ecs_table_t *src_table, 
    int32_t src_index,
    int32_t count,
    bool clear)
{
    int32_t i_old = 0, src_column_count = src_table->bs_count;
    int32_t i_new = 0, dst_column_count = dst_table->bs_count;

    if (!src_column_count && !dst_column_count) {
        return;
    }

    ecs_bitset_t *src_columns = src_table->data.bs_columns;
    ecs_bitset_t *dst_columns = dst_table->data.bs_columns;

    ecs_type_t dst_type = dst_table->type;
    ecs_type_t src_type = src_table->type;

    int32_t offset_new = dst_table->bs_offset;
    int32_t offset_old = src_table->bs_offset;

    ecs_id_t *dst_ids = dst_type.array;
    ecs_id_t *src_ids = src_type.array;

    for (; (i_new < dst_column_count) && (i_old < src_column_count);) {
        ecs_id_t dst_id = dst_ids[i_new + offset_new];
        ecs_id_t src_id = src_ids[i_old + offset_old];

        if (dst_id == src_id) {
            ecs_bitset_t *src_bs = &src_columns[i_old];
            ecs_bitset_t *dst_bs = &dst_columns[i_new];

            flecs_bitset_ensure(dst_bs, dst_index + count);

            int i;
            for (i = 0; i < count; i ++) {
                uint64_t value = flecs_bitset_get(src_bs, src_index + i);
                flecs_bitset_set(dst_bs, dst_index + i, value);
            }

            if (clear) {
                ecs_assert(count == flecs_bitset_count(src_bs), 
                    ECS_INTERNAL_ERROR, NULL);
                flecs_bitset_fini(src_bs);
            }
        } else if (dst_id > src_id) {
            ecs_bitset_t *src_bs = &src_columns[i_old];
            flecs_bitset_fini(src_bs);
        }

        i_new += dst_id <= src_id;
        i_old += dst_id >= src_id;
    }

    /* Clear remaining columns */
    if (clear) {
        for (; (i_old < src_column_count); i_old ++) {
            ecs_bitset_t *src_bs = &src_columns[i_old];
            ecs_assert(count == flecs_bitset_count(src_bs), 
                ECS_INTERNAL_ERROR, NULL);
            flecs_bitset_fini(src_bs);
        }
    }
}

static
void* grow_column(
    ecs_column_t *column,
    ecs_type_info_t *ti,
    int32_t to_add,
    int32_t dst_size,
    bool construct)
{
    ecs_assert(column != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(ti != NULL, ECS_INTERNAL_ERROR, NULL);

    int32_t size = ti->size;
    int32_t count = column->count;
    int32_t src_size = column->size;
    int32_t dst_count = count + to_add;
    bool can_realloc = dst_size != src_size;
    void *result = NULL;

    ecs_assert(dst_size >= dst_count, ECS_INTERNAL_ERROR, NULL);

    /* If the array could possibly realloc and the component has a move action 
     * defined, move old elements manually */
    ecs_move_t move_ctor;
    if (count && can_realloc && (move_ctor = ti->hooks.ctor_move_dtor)) {
        ecs_xtor_t ctor = ti->hooks.ctor;
        ecs_assert(ctor != NULL, ECS_INTERNAL_ERROR, NULL);
        ecs_assert(move_ctor != NULL, ECS_INTERNAL_ERROR, NULL);

        /* Create  vector */
        ecs_column_t dst;
        ecs_storage_init(&dst, size, dst_size);
        dst.count = dst_count;

        void *src_buffer = column->array;
        void *dst_buffer = dst.array;

        /* Move (and construct) existing elements to new vector */
        move_ctor(dst_buffer, src_buffer, count, ti);

        if (construct) {
            /* Construct new element(s) */
            result = ECS_ELEM(dst_buffer, size, count);
            ctor(result, to_add, ti);
        }

        /* Free old vector */
        ecs_storage_fini(column);

        *column = dst;
    } else {
        /* If array won't realloc or has no move, simply add new elements */
        if (can_realloc) {
            ecs_storage_set_size(column, size, dst_size);
        }

        result = ecs_storage_grow(column, size, to_add);

        ecs_xtor_t ctor;
        if (construct && (ctor = ti->hooks.ctor)) {
            /* If new elements need to be constructed and component has a
             * constructor, construct */
            ctor(result, to_add, ti);
        }
    }

    ecs_assert(column->size == dst_size, ECS_INTERNAL_ERROR, NULL);

    return result;
}

static
int32_t grow_data(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_data_t *data,
    int32_t to_add,
    int32_t size,
    const ecs_entity_t *ids)
{
    ecs_assert(table != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(data != NULL, ECS_INTERNAL_ERROR, NULL);

    int32_t cur_count = flecs_table_data_count(data);
    int32_t column_count = table->storage_count;
    int32_t sw_count = table->sw_count;
    int32_t bs_count = table->bs_count;
    ecs_column_t *columns = data->columns;
    ecs_switch_t *sw_columns = data->sw_columns;
    ecs_bitset_t *bs_columns = data->bs_columns; 

    /* Add record to record ptr array */
    ecs_storage_set_size_t(&data->records, ecs_record_t*, size);
    ecs_record_t **r = ecs_storage_last_t(&data->records, ecs_record_t*) + 1;
    data->records.count += to_add;
    if (data->records.size > size) {
        size = data->records.size;
    }

    /* Add entity to column with entity ids */
    ecs_storage_set_size_t(&data->entities, ecs_entity_t, size);
    ecs_entity_t *e = ecs_storage_last_t(&data->entities, ecs_entity_t) + 1;
    data->entities.count += to_add;
    ecs_assert(data->entities.size == size, ECS_INTERNAL_ERROR, NULL);

    /* Initialize entity ids and record ptrs */
    int32_t i;
    if (ids) {
        for (i = 0; i < to_add; i ++) {
            e[i] = ids[i];
        }
    } else {
        ecs_os_memset(e, 0, ECS_SIZEOF(ecs_entity_t) * to_add);
    }
    ecs_os_memset(r, 0, ECS_SIZEOF(ecs_record_t*) * to_add);

    /* Add elements to each column array */
    ecs_type_info_t **type_info = table->type_info;
    for (i = 0; i < column_count; i ++) {
        ecs_column_t *column = &columns[i];
        ecs_type_info_t *ti = type_info[i];
        grow_column(column, ti, to_add, size, true);
        ecs_assert(columns[i].size == size, ECS_INTERNAL_ERROR, NULL);
    }

    /* Add elements to each switch column */
    for (i = 0; i < sw_count; i ++) {
        ecs_switch_t *sw = &sw_columns[i];
        flecs_switch_addn(sw, to_add);
    }

    /* Add elements to each bitset column */
    for (i = 0; i < bs_count; i ++) {
        ecs_bitset_t *bs = &bs_columns[i];
        flecs_bitset_addn(bs, to_add);
    }

    /* If the table is monitored indicate that there has been a change */
    mark_table_dirty(world, table, 0);

    if (!(world->flags & EcsWorldReadonly) && !cur_count) {
        flecs_table_set_empty(world, table);
    }

    /* Return index of first added entity */
    return cur_count;
}

static
void fast_append(
    ecs_type_info_t **type_info,
    ecs_column_t *columns,
    int32_t count)
{
    /* Add elements to each column array */
    int32_t i;
    for (i = 0; i < count; i ++) {
        ecs_type_info_t *ti = type_info[i];
        ecs_column_t *column = &columns[i];
        ecs_storage_append(column, ti->size);
    }
}

int32_t flecs_table_append(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_entity_t entity,
    ecs_record_t *record,
    bool construct)
{
    ecs_assert(table != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(!table->lock, ECS_LOCKED_STORAGE, NULL);

    check_table_sanity(table);

    /* Get count & size before growing entities array. This tells us whether the
     * arrays will realloc */
    ecs_data_t *data = &table->data;
    int32_t count = data->entities.count;
    int32_t column_count = table->storage_count;
    ecs_column_t *columns = table->data.columns;
    
    /* Grow buffer with entity ids, set new element to new entity */
    ecs_entity_t *e = ecs_storage_append_t(&data->entities, ecs_entity_t);
    ecs_assert(e != NULL, ECS_INTERNAL_ERROR, NULL);
    *e = entity;

    /* Add record ptr to array with record ptrs */
    ecs_record_t **r = ecs_storage_append_t(&data->records, ecs_record_t*);
    ecs_assert(r != NULL, ECS_INTERNAL_ERROR, NULL);
    *r = record;
 
    /* If the table is monitored indicate that there has been a change */
    mark_table_dirty(world, table, 0);
    ecs_assert(count >= 0, ECS_INTERNAL_ERROR, NULL);

    ecs_type_info_t **type_info = table->type_info;

    /* Fast path: no switch columns, no lifecycle actions */
    if (!(table->flags & EcsTableIsComplex)) {
        fast_append(type_info, columns, column_count);
        if (!count) {
            flecs_table_set_empty(world, table); /* See below */
        }
        return count;
    }

    int32_t sw_count = table->sw_count;
    int32_t bs_count = table->bs_count;
    ecs_switch_t *sw_columns = data->sw_columns;
    ecs_bitset_t *bs_columns = data->bs_columns;
    ecs_entity_t *entities = data->entities.array;

    /* Reobtain size to ensure that the columns have the same size as the 
     * entities and record vectors. This keeps reasoning about when allocations
     * occur easier. */
    int32_t size = data->entities.size;

    /* Grow component arrays with 1 element */
    int32_t i;
    for (i = 0; i < column_count; i ++) {
        ecs_column_t *column = &columns[i];
        ecs_type_info_t *ti = type_info[i];  
        grow_column(column, ti, 1, size, construct);

        ecs_iter_action_t on_add;
        if (construct && (on_add = ti->hooks.on_add)) {
            on_component_callback(world, table, on_add, EcsOnAdd, column,
                &entities[count], table->storage_ids[i], count, 1, ti);
        }

        ecs_assert(columns[i].size == 
            data->entities.size, ECS_INTERNAL_ERROR, NULL); 
        ecs_assert(columns[i].count == 
            data->entities.count, ECS_INTERNAL_ERROR, NULL);
    }

    /* Add element to each switch column */
    for (i = 0; i < sw_count; i ++) {
        ecs_assert(sw_columns != NULL, ECS_INTERNAL_ERROR, NULL);
        ecs_switch_t *sw = &sw_columns[i];
        flecs_switch_add(sw);
    }

    /* Add element to each bitset column */
    for (i = 0; i < bs_count; i ++) {
        ecs_assert(bs_columns != NULL, ECS_INTERNAL_ERROR, NULL);
        ecs_bitset_t *bs = &bs_columns[i];
        flecs_bitset_addn(bs, 1);
    }    

    /* If this is the first entity in this table, signal queries so that the
     * table moves from an inactive table to an active table. */
    if (!count) {
        flecs_table_set_empty(world, table);
    }

    check_table_sanity(table);

    return count;
}

static
void fast_delete_last(
    ecs_column_t *columns,
    int32_t column_count) 
{
    int i;
    for (i = 0; i < column_count; i ++) {
        ecs_storage_remove_last(&columns[i]);
    }
}

static
void fast_delete(
    ecs_type_info_t **type_info,
    ecs_column_t *columns,
    int32_t column_count,
    int32_t index) 
{
    int i;
    for (i = 0; i < column_count; i ++) {
        ecs_type_info_t *ti = type_info[i];
        ecs_column_t *column = &columns[i];
        ecs_storage_remove(column, ti->size, index);
    }
}

void flecs_table_delete(
    ecs_world_t *world,
    ecs_table_t *table,
    int32_t index,
    bool destruct)
{
    ecs_assert(world != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(table != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(!table->lock, ECS_LOCKED_STORAGE, NULL);

    check_table_sanity(table);

    ecs_data_t *data = &table->data;
    int32_t count = data->entities.count;

    ecs_assert(count > 0, ECS_INTERNAL_ERROR, NULL);
    count --;
    ecs_assert(index <= count, ECS_INTERNAL_ERROR, NULL);

    /* Move last entity id to index */
    ecs_entity_t *entities = data->entities.array;
    ecs_entity_t entity_to_move = entities[count];
    ecs_entity_t entity_to_delete = entities[index];
    entities[index] = entity_to_move;
    ecs_storage_remove_last(&data->entities);

    /* Move last record ptr to index */
    ecs_assert(count < data->records.count, ECS_INTERNAL_ERROR, NULL);

    ecs_record_t **records = data->records.array;
    ecs_record_t *record_to_move = records[count];
    records[index] = record_to_move;
    ecs_storage_remove_last(&data->records); 

    /* Update record of moved entity in entity index */
    if (index != count) {
        if (record_to_move) {
            uint32_t row_flags = record_to_move->row & ECS_ROW_FLAGS_MASK;
            record_to_move->row = ECS_ROW_TO_RECORD(index, row_flags);
            ecs_assert(record_to_move->table != NULL, ECS_INTERNAL_ERROR, NULL);
            ecs_assert(record_to_move->table == table, ECS_INTERNAL_ERROR, NULL);
        }
    }     

    /* If the table is monitored indicate that there has been a change */
    mark_table_dirty(world, table, 0);    

    /* If table is empty, deactivate it */
    if (!count) {
        flecs_table_set_empty(world, table);
    }

    /* Destruct component data */
    ecs_type_info_t **type_info = table->type_info;
    ecs_column_t *columns = data->columns;
    int32_t column_count = table->storage_count;
    int32_t i;

    /* If this is a table without lifecycle callbacks or special columns, take
     * fast path that just remove an element from the array(s) */
    if (!(table->flags & EcsTableIsComplex)) {
        if (index == count) {
            fast_delete_last(columns, column_count);
        } else {
            fast_delete(type_info, columns, column_count, index);
        }

        check_table_sanity(table);
        return;
    }

    ecs_id_t *ids = table->storage_ids;

    /* Last element, destruct & remove */
    if (index == count) {
        /* If table has component destructors, invoke */
        if (destruct && (table->flags & EcsTableHasDtors)) {            
            for (i = 0; i < column_count; i ++) {
                remove_component(world, table, type_info[i], &columns[i], 
                    &entity_to_delete, ids[i], index, 1);
            }
        }

        fast_delete_last(columns, column_count);

    /* Not last element, move last element to deleted element & destruct */
    } else {
        /* If table has component destructors, invoke */
        if (destruct && (table->flags & (EcsTableHasDtors | EcsTableHasMove))) {
            for (i = 0; i < column_count; i ++) {
                ecs_column_t *column = &columns[i];
                ecs_type_info_t *ti = type_info[i];
                ecs_size_t size = ti->size;
                void *dst = ecs_storage_get(column, size, index);
                void *src = ecs_storage_last(column, size);
                
                ecs_iter_action_t on_remove = ti->hooks.on_remove;
                if (on_remove) {
                    on_component_callback(world, table, on_remove, EcsOnRemove,
                        column, &entity_to_delete, ids[i], index, 1, ti);
                }

                ecs_move_t move_dtor = ti->hooks.move_dtor;
                if (move_dtor) {
                    move_dtor(dst, src, 1, ti);
                } else {
                    ecs_os_memcpy(dst, src, size);
                }

                ecs_storage_remove_last(column);
            }
        } else {
            fast_delete(type_info, columns, column_count, index);
        }
    }

    /* Remove elements from switch columns */
    ecs_switch_t *sw_columns = data->sw_columns;
    int32_t sw_count = table->sw_count;
    for (i = 0; i < sw_count; i ++) {
        flecs_switch_remove(&sw_columns[i], index);
    }

    /* Remove elements from bitset columns */
    ecs_bitset_t *bs_columns = data->bs_columns;
    int32_t bs_count = table->bs_count;
    for (i = 0; i < bs_count; i ++) {
        flecs_bitset_remove(&bs_columns[i], index);
    }

    check_table_sanity(table);
}

static
void fast_move(
    ecs_table_t *dst_table,
    int32_t dst_index,
    ecs_table_t *src_table,
    int32_t src_index)
{
    int32_t i_new = 0, dst_column_count = dst_table->storage_count;
    int32_t i_old = 0, src_column_count = src_table->storage_count;
    ecs_id_t *dst_ids = dst_table->storage_ids;
    ecs_id_t *src_ids = src_table->storage_ids;

    ecs_column_t *src_columns = src_table->data.columns;
    ecs_column_t *dst_columns = dst_table->data.columns;

    ecs_type_info_t **dst_type_info = dst_table->type_info;

    for (; (i_new < dst_column_count) && (i_old < src_column_count);) {
        ecs_id_t dst_id = dst_ids[i_new];
        ecs_id_t src_id = src_ids[i_old];

        if (dst_id == src_id) {
            ecs_column_t *dst_column = &dst_columns[i_new];
            ecs_column_t *src_column = &src_columns[i_old];
            ecs_type_info_t *ti = dst_type_info[i_new];
            int32_t size = ti->size;
            void *dst = ecs_storage_get(dst_column, size, dst_index);
            void *src = ecs_storage_get(src_column, size, src_index);
            ecs_os_memcpy(dst, src, size);
        }

        i_new += dst_id <= src_id;
        i_old += dst_id >= src_id;
    }
}

void flecs_table_move(
    ecs_world_t *world,
    ecs_entity_t dst_entity,
    ecs_entity_t src_entity,
    ecs_table_t *dst_table,
    int32_t dst_index,
    ecs_table_t *src_table,
    int32_t src_index,
    bool construct)
{
    ecs_assert(dst_table != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(src_table != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(!dst_table->lock, ECS_LOCKED_STORAGE, NULL);
    ecs_assert(!src_table->lock, ECS_LOCKED_STORAGE, NULL);

    ecs_assert(src_index >= 0, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(dst_index >= 0, ECS_INTERNAL_ERROR, NULL);

    check_table_sanity(dst_table);
    check_table_sanity(src_table);

    if (!((dst_table->flags | src_table->flags) & EcsTableIsComplex)) {
        fast_move(dst_table, dst_index, src_table, src_index);
        check_table_sanity(dst_table);
        check_table_sanity(src_table);
        return;
    }

    move_switch_columns(dst_table, dst_index, src_table, src_index, 1, false);
    move_bitset_columns(dst_table, dst_index, src_table, src_index, 1, false);

    bool same_entity = dst_entity == src_entity;

    ecs_type_info_t **dst_type_info = dst_table->type_info;
    ecs_type_info_t **src_type_info = src_table->type_info;

    int32_t i_new = 0, dst_column_count = dst_table->storage_count;
    int32_t i_old = 0, src_column_count = src_table->storage_count;
    ecs_id_t *dst_ids = dst_table->storage_ids;
    ecs_id_t *src_ids = src_table->storage_ids;

    ecs_column_t *src_columns = src_table->data.columns;
    ecs_column_t *dst_columns = dst_table->data.columns;

    for (; (i_new < dst_column_count) && (i_old < src_column_count);) {
        ecs_id_t dst_id = dst_ids[i_new];
        ecs_id_t src_id = src_ids[i_old];

        if (dst_id == src_id) {
            ecs_column_t *dst_column = &dst_columns[i_new];
            ecs_column_t *src_column = &src_columns[i_old];
            ecs_type_info_t *ti = dst_type_info[i_new];
            int32_t size = ti->size;

            ecs_assert(size != 0, ECS_INTERNAL_ERROR, NULL);
            void *dst = ecs_storage_get(dst_column, size, dst_index);
            void *src = ecs_storage_get(src_column, size, src_index);

            if (same_entity) {
                ecs_move_t move = ti->hooks.ctor_move_dtor;
                if (move) {
                    /* ctor + move + dtor */
                    move(dst, src, 1, ti);
                } else {
                    ecs_os_memcpy(dst, src, size);
                }
            } else {
                ecs_copy_t copy = ti->hooks.copy_ctor;
                if (copy) {
                    copy(dst, src, 1, ti);
                } else {
                    ecs_os_memcpy(dst, src, size);
                }
            }
        } else {
            if (dst_id < src_id) {
                if (construct) {
                    add_component(world, dst_table, dst_type_info[i_new],
                        &dst_columns[i_new], &dst_entity, dst_id, 
                            dst_index, 1);
                }
            } else {
                remove_component(world, src_table, src_type_info[i_old],
                    &src_columns[i_old], &src_entity, src_id, 
                        src_index, 1);
            }
        }

        i_new += dst_id <= src_id;
        i_old += dst_id >= src_id;
    }

    if (construct) {
        for (; (i_new < dst_column_count); i_new ++) {
            add_component(world, dst_table, dst_type_info[i_new],
                &dst_columns[i_new], &dst_entity, dst_ids[i_new], dst_index, 1);
        }
    }

    for (; (i_old < src_column_count); i_old ++) {
        remove_component(world, src_table, src_type_info[i_old],
            &src_columns[i_old], &src_entity, src_ids[i_old], 
                src_index, 1);
    }

    check_table_sanity(dst_table);
    check_table_sanity(src_table);
}

int32_t flecs_table_appendn(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_data_t *data,
    int32_t to_add,
    const ecs_entity_t *ids)
{
    ecs_assert(!table->lock, ECS_LOCKED_STORAGE, NULL);

    check_table_sanity(table);

    int32_t cur_count = flecs_table_data_count(data);
    int32_t result = grow_data(
        world, table, data, to_add, cur_count + to_add, ids);
    check_table_sanity(table);
    return result;
}

void flecs_table_set_size(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_data_t *data,
    int32_t size)
{
    ecs_assert(table != NULL, ECS_LOCKED_STORAGE, NULL);
    ecs_assert(!table->lock, ECS_LOCKED_STORAGE, NULL);

    check_table_sanity(table);

    int32_t cur_count = flecs_table_data_count(data);

    if (cur_count < size) {
        grow_data(world, table, data, 0, size, NULL);
        check_table_sanity(table);
    }
}

bool flecs_table_shrink(
    ecs_world_t *world,
    ecs_table_t *table)
{
    ecs_assert(table != NULL, ECS_LOCKED_STORAGE, NULL);
    ecs_assert(!table->lock, ECS_LOCKED_STORAGE, NULL);
    (void)world;

    check_table_sanity(table);

    ecs_data_t *data = &table->data;
    bool has_payload = data->entities.array != NULL;
    ecs_storage_reclaim_t(&data->entities, ecs_entity_t);
    ecs_storage_reclaim_t(&data->records, ecs_record_t*);

    int32_t i, count = table->storage_count;
    ecs_type_info_t **type_info = table->type_info;
    for (i = 0; i < count; i ++) {
        ecs_column_t *column = &data->columns[i];
        ecs_type_info_t *ti = type_info[i];
        ecs_storage_reclaim(column, ti->size);
    }

    return has_payload;
}

int32_t flecs_table_data_count(
    const ecs_data_t *data)
{
    return data ? data->entities.count : 0;
}

static
void swap_switch_columns(
    ecs_table_t *table,
    ecs_data_t *data,
    int32_t row_1,
    int32_t row_2)
{
    int32_t i = 0, column_count = table->sw_count;
    if (!column_count) {
        return;
    }

    ecs_switch_t *columns = data->sw_columns;

    for (i = 0; i < column_count; i ++) {
        ecs_switch_t *sw = &columns[i];
        flecs_switch_swap(sw, row_1, row_2);
    }
}

static
void swap_bitset_columns(
    ecs_table_t *table,
    ecs_data_t *data,
    int32_t row_1,
    int32_t row_2)
{
    int32_t i = 0, column_count = table->bs_count;
    if (!column_count) {
        return;
    }

    ecs_bitset_t *columns = data->bs_columns;

    for (i = 0; i < column_count; i ++) {
        ecs_bitset_t *bs = &columns[i];
        flecs_bitset_swap(bs, row_1, row_2);
    }
}

void flecs_table_swap(
    ecs_world_t *world,
    ecs_table_t *table,
    int32_t row_1,
    int32_t row_2)
{    
    (void)world;

    ecs_assert(!table->lock, ECS_LOCKED_STORAGE, NULL);
    ecs_assert(row_1 >= 0, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(row_2 >= 0, ECS_INTERNAL_ERROR, NULL);

    check_table_sanity(table);
    
    if (row_1 == row_2) {
        return;
    }

    /* If the table is monitored indicate that there has been a change */
    mark_table_dirty(world, table, 0);    

    ecs_entity_t *entities = table->data.entities.array;
    ecs_entity_t e1 = entities[row_1];
    ecs_entity_t e2 = entities[row_2];

    ecs_record_t **records = table->data.records.array;
    ecs_record_t *record_ptr_1 = records[row_1];
    ecs_record_t *record_ptr_2 = records[row_2];

    ecs_assert(record_ptr_1 != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(record_ptr_2 != NULL, ECS_INTERNAL_ERROR, NULL);

    /* Keep track of whether entity is watched */
    uint32_t flags_1 = ECS_RECORD_TO_ROW_FLAGS(record_ptr_1->row);
    uint32_t flags_2 = ECS_RECORD_TO_ROW_FLAGS(record_ptr_2->row);

    /* Swap entities & records */
    entities[row_1] = e2;
    entities[row_2] = e1;
    record_ptr_1->row = ECS_ROW_TO_RECORD(row_2, flags_1);
    record_ptr_2->row = ECS_ROW_TO_RECORD(row_1, flags_2);
    records[row_1] = record_ptr_2;
    records[row_2] = record_ptr_1;

    swap_switch_columns(table, &table->data, row_1, row_2);
    swap_bitset_columns(table, &table->data, row_1, row_2);  

    ecs_column_t *columns = table->data.columns;
    if (!columns) {
        check_table_sanity(table);
        return;
    }

    ecs_type_info_t **type_info = table->type_info;

    /* Find the maximum size of column elements
     * and allocate a temporary buffer for swapping */
    int32_t i, temp_buffer_size = ECS_SIZEOF(uint64_t), column_count = table->storage_count;
    for (i = 0; i < column_count; i++) {
        ecs_type_info_t* ti = type_info[i];
        temp_buffer_size = ECS_MAX(temp_buffer_size, ti->size);
    }

    void* tmp = ecs_os_alloca(temp_buffer_size);

    /* Swap columns */
    for (i = 0; i < column_count; i ++) {
        ecs_type_info_t *ti = type_info[i];
        int32_t size = ti->size;
        ecs_assert(size != 0, ECS_INTERNAL_ERROR, NULL);

        void *ptr = columns[i].array;

        void *el_1 = ECS_ELEM(ptr, size, row_1);
        void *el_2 = ECS_ELEM(ptr, size, row_2);

        ecs_os_memcpy(tmp, el_1, size);
        ecs_os_memcpy(el_1, el_2, size);
        ecs_os_memcpy(el_2, tmp, size);
    }

    check_table_sanity(table);
}

static
void flecs_merge_column(
    ecs_column_t *dst,
    ecs_column_t *src,
    int32_t size,
    ecs_type_info_t *ti)
{
    int32_t dst_count = dst->count;

    if (!dst_count) {
        ecs_storage_fini(dst);
        *dst = *src;
        src->array = NULL;
        src->count = 0;
        src->size = 0;
    
    /* If the new table is not empty, copy the contents from the
     * src into the dst. */
    } else {
        int32_t src_count = src->count;
        ecs_storage_set_count(dst, size, dst_count + src_count);

        /* Construct new values */
        if (ti) {
            ctor_component(ti, dst, dst_count, src_count);
        }
        
        void *dst_ptr = ECS_ELEM(dst->array, size, dst_count);
        void *src_ptr = src->array;
        
        /* Move values into column */
        ecs_move_t move = NULL;
        if (ti) {
            move = ti->hooks.move;
        }
        if (move) {
            move(dst_ptr, src_ptr, src_count, ti);
        } else {
            ecs_os_memcpy(dst_ptr, src_ptr, size * src_count);
        }

        ecs_storage_fini(src);
    }
}

static
void flecs_merge_table_data(
    ecs_world_t *world,
    ecs_table_t *dst_table,
    ecs_table_t *src_table,
    int32_t src_count,
    int32_t dst_count,
    ecs_data_t *src_data,
    ecs_data_t *dst_data)
{
    int32_t i_new = 0, dst_column_count = dst_table->storage_count;
    int32_t i_old = 0, src_column_count = src_table->storage_count;
    ecs_id_t *dst_ids = dst_table->storage_ids;
    ecs_id_t *src_ids = src_table->storage_ids;

    ecs_type_info_t **dst_type_info = dst_table->type_info;
    ecs_type_info_t **src_type_info = src_table->type_info;

    ecs_column_t *src = src_data->columns;
    ecs_column_t *dst = dst_data->columns;

    ecs_assert(!dst_column_count || dst, ECS_INTERNAL_ERROR, NULL);

    if (!src_count) {
        return;
    }

    /* Merge entities */
    flecs_merge_column(&dst_data->entities, &src_data->entities, 
        ECS_SIZEOF(ecs_entity_t), NULL);
    ecs_assert(dst_data->entities.count == src_count + dst_count, 
        ECS_INTERNAL_ERROR, NULL);

    /* Merge record pointers */
    flecs_merge_column(&dst_data->records, &src_data->records, 
        ECS_SIZEOF(ecs_record_t*), 0);

    for (; (i_new < dst_column_count) && (i_old < src_column_count); ) {
        ecs_id_t dst_id = dst_ids[i_new];
        ecs_id_t src_id = src_ids[i_old];
        ecs_type_info_t *dst_ti = dst_type_info[i_new];
        int32_t size = dst_ti->size;
        ecs_assert(size != 0, ECS_INTERNAL_ERROR, NULL);

        if (dst_id == src_id) {
            flecs_merge_column(&dst[i_new], &src[i_old], size, dst_ti);
            mark_table_dirty(world, dst_table, i_new + 1);
            
            i_new ++;
            i_old ++;
        } else if (dst_id < src_id) {
            /* New column, make sure vector is large enough. */
            ecs_column_t *column = &dst[i_new];
            ecs_storage_set_count(column, size, src_count + dst_count);
            ctor_component(dst_ti, column, 0, src_count + dst_count);
            i_new ++;
        } else if (dst_id > src_id) {
            /* Old column does not occur in new table, destruct */
            ecs_column_t *column = &src[i_old];
            dtor_component(src_type_info[i_old], column, 0, src_count);
            ecs_storage_fini(column);
            i_old ++;
        }
    }

    move_switch_columns(dst_table, dst_count, src_table, 0, src_count, true);
    move_bitset_columns(dst_table, dst_count, src_table, 0, src_count, true);

    /* Initialize remaining columns */
    for (; i_new < dst_column_count; i_new ++) {
        ecs_column_t *column = &dst[i_new];
        ecs_type_info_t *ti = dst_type_info[i_new];
        int32_t size = ti->size;        
        ecs_assert(size != 0, ECS_INTERNAL_ERROR, NULL);
        ecs_storage_set_count(column, size, src_count + dst_count);
        ctor_component(ti, column, 0, src_count + dst_count);
    }

    /* Destruct remaining columns */
    for (; i_old < src_column_count; i_old ++) {
        ecs_column_t *column = &src[i_old];
        dtor_component(src_type_info[i_old], column, 0, src_count);
        ecs_storage_fini(column);
    }    

    /* Mark entity column as dirty */
    mark_table_dirty(world, dst_table, 0); 
}

int32_t ecs_table_count(
    const ecs_table_t *table)
{
    ecs_assert(table != NULL, ECS_INTERNAL_ERROR, NULL);
    return flecs_table_data_count(&table->data);
}

void flecs_table_merge(
    ecs_world_t *world,
    ecs_table_t *dst_table,
    ecs_table_t *src_table,
    ecs_data_t *dst_data,
    ecs_data_t *src_data)
{
    ecs_assert(src_table != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(!src_table->lock, ECS_LOCKED_STORAGE, NULL);

    check_table_sanity(dst_table);
    check_table_sanity(src_table);
    
    bool move_data = false;
    
    /* If there is nothing to merge to, just clear the old table */
    if (!dst_table) {
        flecs_table_clear_data(world, src_table, src_data);
        check_table_sanity(src_table);
        return;
    } else {
        ecs_assert(!dst_table->lock, ECS_LOCKED_STORAGE, NULL);
    }

    /* If there is no data to merge, drop out */
    if (!src_data) {
        return;
    }

    if (!dst_data) {
        dst_data = &dst_table->data;
        if (dst_table == src_table) {
            move_data = true;
        }
    }

    ecs_entity_t *src_entities = src_data->entities.array;
    int32_t src_count = src_data->entities.count;
    int32_t dst_count = dst_data->entities.count;
    ecs_record_t **src_records = src_data->records.array;

    /* First, update entity index so old entities point to new type */
    int32_t i;
    for(i = 0; i < src_count; i ++) {
        ecs_record_t *record;
        if (dst_table != src_table) {
            record = src_records[i];
            ecs_assert(record != NULL, ECS_INTERNAL_ERROR, NULL);
        } else {
            record = flecs_entities_ensure(world, src_entities[i]);
        }

        uint32_t flags = ECS_RECORD_TO_ROW_FLAGS(record->row);
        record->row = ECS_ROW_TO_RECORD(dst_count + i, flags);
        record->table = dst_table;
    }

    /* Merge table columns */
    if (move_data) {
        *dst_data = *src_data;
    } else {
        flecs_merge_table_data(world, dst_table, src_table, src_count, dst_count, 
            src_data, dst_data);
    }

    if (src_count) {
        if (!dst_count) {
            flecs_table_set_empty(world, dst_table);
        }
        flecs_table_set_empty(world, src_table);
    }

    check_table_sanity(src_table);
    check_table_sanity(dst_table);
}

void flecs_table_replace_data(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_data_t *data)
{
    int32_t prev_count = 0;
    ecs_data_t *table_data = &table->data;
    ecs_assert(!data || data != table_data, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(!table->lock, ECS_LOCKED_STORAGE, NULL);

    check_table_sanity(table);

    prev_count = table_data->entities.count;
    run_on_remove(world, table, table_data);
    flecs_table_clear_data(world, table, table_data);

    if (data) {
        table->data = *data;
    } else {
        flecs_table_init_data(table);
    }

    int32_t count = ecs_table_count(table);

    if (!prev_count && count) {
        flecs_table_set_empty(world, table);
    } else if (prev_count && !count) {
        flecs_table_set_empty(world, table);
    }

    check_table_sanity(table);
}

int32_t* flecs_table_get_dirty_state(
    ecs_table_t *table)
{    
    if (!table->dirty_state) {
        int32_t column_count = table->storage_count;
        table->dirty_state = ecs_os_malloc_n( int32_t, column_count + 1);
        ecs_assert(table->dirty_state != NULL, ECS_INTERNAL_ERROR, NULL);
        
        for (int i = 0; i < column_count + 1; i ++) {
            table->dirty_state[i] = 1;
        }
    }
    return table->dirty_state;
}

int32_t* flecs_table_get_monitor(
    ecs_table_t *table)
{
    int32_t *dirty_state = flecs_table_get_dirty_state(table);
    ecs_assert(dirty_state != NULL, ECS_INTERNAL_ERROR, NULL);

    int32_t column_count = table->storage_count;
    return ecs_os_memdup(dirty_state, (column_count + 1) * ECS_SIZEOF(int32_t));
}

void flecs_table_notify(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_table_event_t *event)
{
    if (world->flags & EcsWorldFini) {
        return;
    }

    switch(event->kind) {
    case EcsTableTriggersForId:
        notify_trigger(world, table, event->event);
        break;
    case EcsTableNoTriggersForId:
        break;
    }
}

void ecs_table_lock(
    ecs_world_t *world,
    ecs_table_t *table)
{
    if (table) {
        if (ecs_poly_is(world, ecs_world_t) && !(world->flags & EcsWorldReadonly)) {
            table->lock ++;
        }
    }
}

void ecs_table_unlock(
    ecs_world_t *world,
    ecs_table_t *table)
{
    if (table) {
        if (ecs_poly_is(world, ecs_world_t) && !(world->flags & EcsWorldReadonly)) {
            table->lock --;
            ecs_assert(table->lock >= 0, ECS_INVALID_OPERATION, NULL);
        }
    }
}

bool ecs_table_has_module(
    ecs_table_t *table)
{
    return table->flags & EcsTableHasModule;
}

ecs_column_t* ecs_table_column_for_id(
    const ecs_world_t *world,
    const ecs_table_t *table,
    ecs_id_t id)
{
    ecs_table_t *storage_table = table->storage_table;
    if (!storage_table) {
        return NULL;
    }

    ecs_table_record_t *tr = flecs_table_record_get(world, storage_table, id);
    if (tr) {
        return &table->data.columns[tr->column];
    }

    return NULL;
}

const ecs_type_t* ecs_table_get_type(
    const ecs_table_t *table)
{
    if (table) {
        return &table->type;
    } else {
        return NULL;
    }
}

ecs_table_t* ecs_table_get_storage_table(
    const ecs_table_t *table)
{
    return table->storage_table;
}

int32_t ecs_table_type_to_storage_index(
    const ecs_table_t *table,
    int32_t index)
{
    ecs_assert(index >= 0, ECS_INVALID_PARAMETER, NULL);
    ecs_check(index < table->type.count, ECS_INVALID_PARAMETER, NULL);
    int32_t *storage_map = table->storage_map;
    if (storage_map) {
        return storage_map[index];
    }
error:
    return -1;
}

int32_t ecs_table_storage_to_type_index(
    const ecs_table_t *table,
    int32_t index)
{
    ecs_check(index < table->storage_count, ECS_INVALID_PARAMETER, NULL);
    ecs_check(table->storage_map != NULL, ECS_INVALID_PARAMETER, NULL);
    int32_t offset = table->type.count;
    return table->storage_map[offset + index];
error:
    return -1;
}

int32_t flecs_table_column_to_union_index(
    const ecs_table_t *table,
    int32_t column)
{
    int32_t sw_count = table->sw_count;
    if (sw_count) {
        int32_t sw_offset = table->sw_offset;
        if (column >= sw_offset && column < (sw_offset + sw_count)){
            return column - sw_offset;
        }
    }
    return -1;
}

ecs_record_t* ecs_record_find(
    const ecs_world_t *world,
    ecs_entity_t entity)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(entity != 0, ECS_INVALID_PARAMETER, NULL);

    world = ecs_get_world(world);

    ecs_record_t *r = flecs_entities_get(world, entity);
    if (r) {
        return r;
    }
error:
    return NULL;
}

void* ecs_table_get_column(
    ecs_table_t *table,
    int32_t index)
{
    ecs_check(table != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(index < table->type.count, ECS_INVALID_PARAMETER, NULL);

    int32_t storage_index = table->storage_map[index];
    if (storage_index == -1) {
        return NULL;
    }

    return table->data.columns[storage_index].array;
error:
    return NULL;
}

void* ecs_record_get_column(
    const ecs_record_t *r,
    int32_t column,
    size_t c_size)
{
    (void)c_size;
    ecs_table_t *table = r->table;

    ecs_check(column < table->storage_count, ECS_INVALID_PARAMETER, NULL);
    ecs_type_info_t *ti = table->type_info[column];
    ecs_column_t *c = &table->data.columns[column];
    ecs_assert(c != NULL, ECS_INTERNAL_ERROR, NULL);

    ecs_check(!flecs_utosize(c_size) || flecs_utosize(c_size) == ti->size, 
        ECS_INVALID_PARAMETER, NULL);

    return ecs_storage_get(c, ti->size, ECS_RECORD_TO_ROW(r->row));
error:
    return NULL;
}

void ecs_table_swap_rows(
    ecs_world_t* world,
    ecs_table_t* table,
    int32_t row_1,
    int32_t row_2)
{
    flecs_table_swap(world, table, row_1, row_2);
}


static const char* mixin_kind_str[] = {
    [EcsMixinBase] = "base (should never be requested by application)",
    [EcsMixinWorld] = "world",
    [EcsMixinEntity] = "entity",
    [EcsMixinObservable] = "observable",
    [EcsMixinIterable] = "iterable",
    [EcsMixinDtor] = "dtor",
    [EcsMixinMax] = "max (should never be requested by application)"
};

ecs_mixins_t ecs_world_t_mixins = {
    .type_name = "ecs_world_t",
    .elems = {
        [EcsMixinWorld] = offsetof(ecs_world_t, self),
        [EcsMixinObservable] = offsetof(ecs_world_t, observable),
        [EcsMixinIterable] = offsetof(ecs_world_t, iterable)
    }
};

ecs_mixins_t ecs_stage_t_mixins = {
    .type_name = "ecs_stage_t",
    .elems = {
        [EcsMixinBase] = offsetof(ecs_stage_t, world),
        [EcsMixinWorld] = offsetof(ecs_stage_t, world)
    }
};

ecs_mixins_t ecs_query_t_mixins = {
    .type_name = "ecs_query_t",
    .elems = {
        [EcsMixinWorld] = offsetof(ecs_query_t, world),
        [EcsMixinEntity] = offsetof(ecs_query_t, entity),
        [EcsMixinIterable] = offsetof(ecs_query_t, iterable),
        [EcsMixinDtor] = offsetof(ecs_query_t, dtor)
    }
};

ecs_mixins_t ecs_observer_t_mixins = {
    .type_name = "ecs_observer_t",
    .elems = {
        [EcsMixinWorld] = offsetof(ecs_observer_t, world),
        [EcsMixinEntity] = offsetof(ecs_observer_t, entity),
        [EcsMixinDtor] = offsetof(ecs_observer_t, dtor)
    }
};

ecs_mixins_t ecs_filter_t_mixins = {
    .type_name = "ecs_filter_t",
    .elems = {
        [EcsMixinIterable] = offsetof(ecs_filter_t, iterable)
    }
};

static
void* get_mixin(
    const ecs_poly_t *poly,
    ecs_mixin_kind_t kind)
{
    ecs_assert(poly != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(kind < EcsMixinMax, ECS_INVALID_PARAMETER, NULL);
    
    const ecs_header_t *hdr = poly;
    ecs_assert(hdr != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(hdr->magic == ECS_OBJECT_MAGIC, ECS_INVALID_PARAMETER, NULL);

    const ecs_mixins_t *mixins = hdr->mixins;
    if (!mixins) {
        /* Object has no mixins */
        goto not_found;
    }

    ecs_size_t offset = mixins->elems[kind];
    if (offset == 0) {
        /* Object has mixins but not the requested one. Try to find the mixin
         * in the poly's base */
        goto find_in_base;
    }

    /* Object has mixin, return its address */
    return ECS_OFFSET(hdr, offset);

find_in_base:    
    if (offset) {
        /* If the poly has a base, try to find the mixin in the base */
        ecs_poly_t *base = *(ecs_poly_t**)ECS_OFFSET(hdr, offset);
        if (base) {
            return get_mixin(base, kind);
        }
    }
    
not_found:
    /* Mixin wasn't found for poly */
    return NULL;
}

static
void* assert_mixin(
    const ecs_poly_t *poly,
    ecs_mixin_kind_t kind)
{
    void *ptr = get_mixin(poly, kind);
    if (!ptr) {
        const ecs_header_t *header = poly;
        const ecs_mixins_t *mixins = header->mixins;
        ecs_err("%s not available for type %s", 
            mixin_kind_str[kind],
            mixins ? mixins->type_name : "unknown");
        ecs_os_abort();
    }

    return ptr;
}

void* _ecs_poly_init(
    ecs_poly_t *poly,
    int32_t type,
    ecs_size_t size,
    ecs_mixins_t *mixins)
{
    ecs_assert(poly != NULL, ECS_INVALID_PARAMETER, NULL);

    ecs_header_t *hdr = poly;
    ecs_os_memset(poly, 0, size);

    hdr->magic = ECS_OBJECT_MAGIC;
    hdr->type = type;
    hdr->mixins = mixins;

    return poly;
}

void _ecs_poly_fini(
    ecs_poly_t *poly,
    int32_t type)
{
    ecs_assert(poly != NULL, ECS_INVALID_PARAMETER, NULL);
    (void)type;

    ecs_header_t *hdr = poly;

    /* Don't deinit poly that wasn't initialized */
    ecs_assert(hdr->magic == ECS_OBJECT_MAGIC, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(hdr->type == type, ECS_INVALID_PARAMETER, NULL);
    hdr->magic = 0;
}

EcsPoly* _ecs_poly_bind(
    ecs_world_t *world,
    ecs_entity_t entity,
    ecs_entity_t tag)
{
    /* Add tag to the entity for easy querying. This will make it possible to
     * query for `Query` instead of `(Poly, Query) */
    if (!ecs_has_id(world, entity, tag)) {
        ecs_add_id(world, entity, tag);
    }

    /* Never defer creation of a poly object */
    bool deferred = false;
    if (ecs_is_deferred(world)) {
        deferred = true;
        ecs_defer_suspend(world);
    }

    /* If this is a new poly, leave the actual creation up to the caller so they
     * call tell the difference between a create or an update */
    EcsPoly *result = ecs_get_mut_pair(world, entity, EcsPoly, tag);

    if (deferred) {
        ecs_defer_resume(world);
    }

    return result;
}

void _ecs_poly_modified(
    ecs_world_t *world,
    ecs_entity_t entity,
    ecs_entity_t tag)
{
    ecs_modified_pair(world, entity, ecs_id(EcsPoly), tag);
}

const EcsPoly* _ecs_poly_bind_get(
    const ecs_world_t *world,
    ecs_entity_t entity,
    ecs_entity_t tag)
{
    return ecs_get_pair(world, entity, EcsPoly, tag);
}

ecs_poly_t* _ecs_poly_get(
    const ecs_world_t *world,
    ecs_entity_t entity,
    ecs_entity_t tag)
{
    const EcsPoly *p = _ecs_poly_bind_get(world, entity, tag);
    if (p) {
        return p->poly;
    }
    return NULL;
}

#define assert_object(cond, file, line, type_name)\
    _ecs_assert((cond), ECS_INVALID_PARAMETER, #cond, file, line, type_name);\
    assert(cond)

#ifndef FLECS_NDEBUG
void* _ecs_poly_assert(
    const ecs_poly_t *poly,
    int32_t type,
    const char *file,
    int32_t line)
{
    assert_object(poly != NULL, file, line, 0);
    
    const ecs_header_t *hdr = poly;
    const char *type_name = hdr->mixins->type_name;
    assert_object(hdr->magic == ECS_OBJECT_MAGIC, file, line, type_name);
    assert_object(hdr->type == type, file, line, type_name);
    return (ecs_poly_t*)poly;
}
#endif

bool _ecs_poly_is(
    const ecs_poly_t *poly,
    int32_t type)
{
    ecs_assert(poly != NULL, ECS_INVALID_PARAMETER, NULL);

    const ecs_header_t *hdr = poly;
    ecs_assert(hdr->magic == ECS_OBJECT_MAGIC, ECS_INVALID_PARAMETER, NULL);
    return hdr->type == type;    
}

ecs_iterable_t* ecs_get_iterable(
    const ecs_poly_t *poly)
{
    return (ecs_iterable_t*)assert_mixin(poly, EcsMixinIterable);
}

ecs_observable_t* ecs_get_observable(
    const ecs_poly_t *poly)
{
    return (ecs_observable_t*)assert_mixin(poly, EcsMixinObservable);
}

const ecs_world_t* ecs_get_world(
    const ecs_poly_t *poly)
{
    return *(ecs_world_t**)assert_mixin(poly, EcsMixinWorld);
}

ecs_poly_dtor_t* ecs_get_dtor(
    const ecs_poly_t *poly)
{
    return (ecs_poly_dtor_t*)assert_mixin(poly, EcsMixinDtor);
}


void ecs_storage_init(
    ecs_column_t *storage,
    ecs_size_t size,
    int32_t elem_count)
{
    ecs_assert(size != 0, ECS_INVALID_PARAMETER, NULL);
    storage->array = NULL;
    storage->count = 0;
    if (elem_count) {
        storage->array = ecs_os_malloc(size * elem_count);
    }
    storage->size = elem_count;
}

void ecs_storage_fini(
    ecs_column_t *storage)
{
    ecs_os_free(storage->array);
    storage->array = NULL;
    storage->count = 0;
    storage->size = 0;
}

int32_t ecs_storage_count(
    ecs_column_t *storage)
{
    return storage->count;
}

int32_t ecs_storage_size(
    ecs_column_t *storage)
{
    return storage->size;
}

void* ecs_storage_get(
    ecs_column_t *storage,
    ecs_size_t size,
    int32_t index)
{
    ecs_assert(index < storage->count, ECS_OUT_OF_RANGE, NULL);
    return ECS_ELEM(storage->array, size, index);
}

void* ecs_storage_last(
    ecs_column_t *storage,
    ecs_size_t size)
{
    return ECS_ELEM(storage->array, size, storage->count - 1);
}

void* ecs_storage_first(
    ecs_column_t *storage)
{
    return storage->array;
}

void* ecs_storage_append(
    ecs_column_t *storage,
    ecs_size_t size)
{
    int32_t count = storage->count;
    if (storage->size == count) {
        ecs_storage_set_size(storage, size, count + 1);
    }
    storage->count = count + 1;
    return ECS_ELEM(storage->array, size, count);
}

void ecs_storage_remove(
    ecs_column_t *storage,
    ecs_size_t size,
    int32_t index)
{
    ecs_assert(index < storage->count, ECS_OUT_OF_RANGE, NULL);
    if (index == --storage->count) {
        return;
    }

    ecs_os_memcpy(
        ECS_ELEM(storage->array, size, index),
        ECS_ELEM(storage->array, size, storage->count),
        size);
}

void ecs_storage_remove_last(
    ecs_column_t *storage)
{
    storage->count --;
}

ecs_column_t ecs_storage_copy(
    ecs_column_t *storage,
    ecs_size_t size)
{
    return (ecs_column_t) {
        .count = storage->count,
        .size = storage->size,
        .array = ecs_os_memdup(storage->array, storage->size * size)
    };
}

void ecs_storage_reclaim(
    ecs_column_t *storage,
    ecs_size_t size)
{
    int32_t count = storage->count;
    if (count < storage->size) {
        if (count) {
            storage->array = ecs_os_realloc(storage->array, size * count);
            storage->size = count;
        } else {
            ecs_storage_fini(storage);
        }
    }
}

void ecs_storage_set_size(
    ecs_column_t *storage,
    ecs_size_t size,
    int32_t elem_count)
{
    if (storage->size != elem_count) {
        if (elem_count < storage->count) {
            elem_count = storage->count;
        }

        elem_count = flecs_next_pow_of_2(elem_count);
        if (elem_count < 2) {
            elem_count = 2;
        }
        if (elem_count != storage->size) {
            storage->array = ecs_os_realloc(storage->array, size * elem_count);
            storage->size = elem_count;
        }
    }
}

void ecs_storage_set_count(
    ecs_column_t *storage,
    ecs_size_t size,
    int32_t elem_count)
{
    if (storage->count != elem_count) {
        if (storage->size < elem_count) {
            ecs_storage_set_size(storage, size, elem_count);
        }

        storage->count = elem_count;
    }
}

void* ecs_storage_grow(
    ecs_column_t *storage,
    ecs_size_t size,
    int32_t elem_count)
{
    ecs_assert(elem_count > 0, ECS_INTERNAL_ERROR, NULL);
    int32_t count = storage->count;
    ecs_storage_set_count(storage, size, count + elem_count);
    return ECS_ELEM(storage->array, size, count);
}

#include <ctype.h>

static
void flecs_notify_on_add(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_table_t *other_table,
    int32_t row,
    int32_t count,
    ecs_table_diff_t *diff,
    bool run_on_set);

static
const ecs_entity_t* new_w_data(
    ecs_world_t *world,
    ecs_table_t *table,
    const ecs_entity_t *entities,
    ecs_type_t *component_ids,
    int32_t count,
    void **c_info,
    bool move,
    int32_t *row_out,
    ecs_table_diff_t *diff);

static
void* get_component_w_index(
    ecs_table_t *table,
    int32_t column_index,
    int32_t row)
{
    ecs_check(column_index < table->storage_count, ECS_NOT_A_COMPONENT, NULL);
    ecs_type_info_t *ti = table->type_info[column_index];
    ecs_column_t *column = &table->data.columns[column_index];
    return ecs_storage_get(column, ti->size, row);;
error:
    return NULL;
}

static
void* get_component(
    const ecs_world_t *world,
    ecs_table_t *table,
    int32_t row,
    ecs_id_t id)
{
    ecs_assert(table != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(id != 0, ECS_INVALID_PARAMETER, NULL);

    if (!table->storage_table) {
        ecs_check(ecs_search(world, table, id, 0) == -1, 
            ECS_NOT_A_COMPONENT, NULL);
        return NULL;
    }

    ecs_table_record_t *tr = flecs_table_record_get(
        world, table->storage_table, id);
    if (!tr) {
        ecs_check(ecs_search(world, table, id, 0) == -1, 
            ECS_NOT_A_COMPONENT, NULL);
       return NULL;
    }

    return get_component_w_index(table, tr->column, row);
error:
    return NULL;
}

static
void* get_base_component(
    const ecs_world_t *world,
    ecs_table_t *table,
    ecs_id_t id,
    ecs_id_record_t *table_index,
    int32_t recur_depth)
{
    /* Cycle detected in IsA relationship */
    ecs_check(recur_depth < ECS_MAX_RECURSION, ECS_INVALID_PARAMETER, NULL);

    /* Table (and thus entity) does not have component, look for base */
    if (!(table->flags & EcsTableHasIsA)) {
        return NULL;
    }

    /* Exclude Name */
    if (id == ecs_pair(ecs_id(EcsIdentifier), EcsName)) {
        return NULL;
    }

    /* Table should always be in the table index for (IsA, *), otherwise the
     * HasBase flag should not have been set */
    const ecs_table_record_t *tr_isa = flecs_id_record_get_table(
        world->idr_isa_wildcard, table);
    ecs_check(tr_isa != NULL, ECS_INTERNAL_ERROR, NULL);

    ecs_type_t type = table->type;
    ecs_id_t *ids = type.array;
    int32_t i = tr_isa->column, end = tr_isa->count + tr_isa->column;
    void *ptr = NULL;

    do {
        ecs_id_t pair = ids[i ++];
        ecs_entity_t base = ecs_pair_second(world, pair);

        ecs_record_t *r = flecs_entities_get(world, base);
        if (!r) {
            continue;
        }

        table = r->table;
        if (!table) {
            continue;
        }

        const ecs_table_record_t *tr = NULL;

        ecs_table_t *storage_table = table->storage_table;
        if (storage_table) {
            tr = flecs_id_record_get_table(table_index, storage_table);
        } else {
            ecs_check(!ecs_owns_id(world, base, id), 
                ECS_NOT_A_COMPONENT, NULL);
        }

        if (!tr) {
            ptr = get_base_component(world, table, id, table_index, 
                recur_depth + 1);
        } else {
            int32_t row = ECS_RECORD_TO_ROW(r->row);
            ptr = get_component_w_index(table, tr->column, row);
        }
    } while (!ptr && (i < end));

    return ptr;
error:
    return NULL;
}

static
const ecs_type_info_t *get_c_info(
    ecs_world_t *world,
    ecs_entity_t component)
{
    ecs_entity_t real_id = ecs_get_typeid(world, component);
    if (real_id) {
        return flecs_type_info_get(world, real_id);
    } else {
        return NULL;
    }
}

/* Utilities for creating a diff struct on the fly between two arbitrary tables.
 * This is temporary code that will eventually be replaced by a cache that 
 * stores the diff between two archetypes. */

typedef struct {
    ecs_type_t type;
    ecs_size_t size;
} ecs_type_buffer_t;

typedef struct {
    ecs_type_buffer_t added;
    ecs_type_buffer_t removed;
    ecs_type_buffer_t on_set;
    ecs_type_buffer_t un_set;
} ecs_diff_buffer_t;

static
void ids_merge(
    ecs_type_buffer_t *ids,
    ecs_type_t *add)
{
    if (!add || !add->count) {
        return;
    }
    
    int32_t new_count = ids->type.count + add->count;
    if (new_count > ids->size) {
        ids->size = flecs_next_pow_of_2(new_count);
        ecs_id_t *arr = ecs_os_malloc_n(ecs_id_t, ids->size);
        ecs_os_memcpy_n(arr, ids->type.array, ecs_id_t, ids->type.count);

        if (ids->type.count > ECS_ID_CACHE_SIZE) {
            ecs_os_free(ids->type.array);
        }
        
        ids->type.array = arr;
    }

    ecs_os_memcpy_n(&ids->type.array[ids->type.count], 
        add->array, ecs_id_t, add->count);
    ids->type.count += add->count;
}

#define ECS_DIFF_INIT {\
    .added = { .type = { .count = 0, .array = (ecs_id_t[ECS_ID_CACHE_SIZE]){0}}, .size = ECS_ID_CACHE_SIZE },\
    .removed = { .type = { .count = 0, .array = (ecs_id_t[ECS_ID_CACHE_SIZE]){0}}, .size = ECS_ID_CACHE_SIZE },\
    .on_set = { .type = { .count = 0, .array = (ecs_id_t[ECS_ID_CACHE_SIZE]){0}}, .size = ECS_ID_CACHE_SIZE },\
    .un_set = { .type = { .count = 0, .array = (ecs_id_t[ECS_ID_CACHE_SIZE]){0}}, .size = ECS_ID_CACHE_SIZE },\
}

static
void diff_append(
    ecs_diff_buffer_t *dst,
    ecs_table_diff_t *src)
{
    ids_merge(&dst->added, &src->added);
    ids_merge(&dst->removed, &src->removed);
    ids_merge(&dst->on_set, &src->on_set);
    ids_merge(&dst->un_set, &src->un_set);
}

static
void diff_free(
    ecs_diff_buffer_t *diff)
{
    if (diff->added.type.count > ECS_ID_CACHE_SIZE) {
        ecs_os_free(diff->added.type.array);
    }
    if (diff->removed.type.count > ECS_ID_CACHE_SIZE) {
        ecs_os_free(diff->removed.type.array);
    }
    if (diff->on_set.type.count > ECS_ID_CACHE_SIZE) {
        ecs_os_free(diff->on_set.type.array);
    }
    if (diff->un_set.type.count > ECS_ID_CACHE_SIZE) {
        ecs_os_free(diff->un_set.type.array);
    }
}

static
ecs_table_diff_t diff_to_table_diff(
    ecs_diff_buffer_t *diff)
{
    return (ecs_table_diff_t){
        .added = diff->added.type,
        .removed = diff->removed.type,
        .on_set = diff->on_set.type,
        .un_set = diff->un_set.type
    };
}

static
ecs_table_t* table_append(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_id_t id,
    ecs_diff_buffer_t *diff)
{
    ecs_table_diff_t temp_diff;
    table = flecs_table_traverse_add(world, table, &id, &temp_diff);
    ecs_check(table != NULL, ECS_INVALID_PARAMETER, NULL);
    diff_append(diff, &temp_diff);
    return table;
error:
    return NULL;
}

static
void flecs_notify(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_table_t *other_table,
    int32_t row,
    int32_t count,
    ecs_entity_t event,
    ecs_type_t *ids,
    ecs_entity_t relationship)
{
    flecs_emit(world, world, &(ecs_event_desc_t){
        .event = event,
        .ids = ids,
        .table = table,
        .other_table = other_table,
        .offset = row,
        .count = count,
        .observable = world,
        .relationship = relationship
    });
}

static
void instantiate(
    ecs_world_t *world,
    ecs_entity_t base,
    ecs_table_t *table,
    int32_t row,
    int32_t count);

static
void instantiate_children(
    ecs_world_t *world,
    ecs_entity_t base,
    ecs_table_t *table,
    int32_t row,
    int32_t count,
    ecs_table_t *child_table)
{
    if (!ecs_table_count(child_table)) {
        return;
    }

    ecs_type_t type = child_table->type;
    ecs_data_t *child_data = &child_table->data;

    ecs_entity_t *ids = type.array;
    int32_t type_count = type.count;

    /* Instantiate child table for each instance */

    /* Create component array for creating the table */
    ecs_type_t components = {
        .array = ecs_os_alloca_n(ecs_entity_t, type_count + 1)
    };

    void **component_data = ecs_os_alloca_n(void*, type_count + 1);

    /* Copy in component identifiers. Find the base index in the component
     * array, since we'll need this to replace the base with the instance id */
    int j, i, childof_base_index = -1, pos = 0;
    for (i = 0; i < type_count; i ++) {
        ecs_id_t id = ids[i];

        /* Make sure instances don't have EcsPrefab */
        if (id == EcsPrefab) {
            continue;
        }

        /* Keep track of the element that creates the ChildOf relationship with
         * the prefab parent. We need to replace this element to make sure the
         * created children point to the instance and not the prefab */ 
        if (ECS_HAS_RELATION(id, EcsChildOf) && (ECS_PAIR_SECOND(id) == base)) {
            childof_base_index = pos;
        }

        int32_t storage_index = ecs_table_type_to_storage_index(child_table, i);
        if (storage_index != -1) {
            ecs_column_t *column = &child_data->columns[storage_index];
            component_data[pos] = ecs_storage_first(column);
        } else {
            component_data[pos] = NULL;
        }

        components.array[pos] = id;
        pos ++;
    }

    /* Table must contain children of base */
    ecs_assert(childof_base_index != -1, ECS_INTERNAL_ERROR, NULL);

    /* If children are added to a prefab, make sure they are prefabs too */
    if (table->flags & EcsTableIsPrefab) {
        components.array[pos] = EcsPrefab;
        component_data[pos] = NULL;
        pos ++;
    }

    components.count = pos;

    /* Instantiate the prefab child table for each new instance */
    ecs_entity_t *entities = ecs_storage_first(&table->data.entities);
    int32_t child_count = ecs_storage_count(&child_data->entities);

    for (i = row; i < count + row; i ++) {
        ecs_entity_t instance = entities[i];
        ecs_diff_buffer_t diff = ECS_DIFF_INIT;
        ecs_table_t *i_table = NULL;
 
        /* Replace ChildOf element in the component array with instance id */
        components.array[childof_base_index] = ecs_pair(EcsChildOf, instance);

        /* Find or create table */
        for (j = 0; j < components.count; j ++) {
            i_table = table_append(world, i_table, components.array[j], &diff);
        }

        ecs_assert(i_table != NULL, ECS_INTERNAL_ERROR, NULL);
        ecs_assert(i_table->type.count == components.count,
            ECS_INTERNAL_ERROR, NULL);

        /* The instance is trying to instantiate from a base that is also
         * its parent. This would cause the hierarchy to instantiate itself
         * which would cause infinite recursion. */
        ecs_entity_t *children = ecs_storage_first(&child_data->entities);

#ifdef FLECS_DEBUG
        for (j = 0; j < child_count; j ++) {
            ecs_entity_t child = children[j];        
            ecs_check(child != instance, ECS_INVALID_PARAMETER, NULL);
        }
#else
        /* Bit of boilerplate to ensure that we don't get warnings about the
         * error label not being used. */
        ecs_check(true, ECS_INVALID_OPERATION, NULL);
#endif

        /* Create children */
        int32_t child_row;
        ecs_table_diff_t table_diff = diff_to_table_diff(&diff);
        new_w_data(world, i_table, NULL, &components, child_count, 
            component_data, false, &child_row, &table_diff);
        diff_free(&diff);

        /* If prefab child table has children itself, recursively instantiate */
        for (j = 0; j < child_count; j ++) {
            ecs_entity_t child = children[j];
            instantiate(world, child, i_table, child_row + j, 1);
        }
    }   
error:
    return;    
}

static
void instantiate(
    ecs_world_t *world,
    ecs_entity_t base,
    ecs_table_t *table,
    int32_t row,
    int32_t count)
{
    ecs_table_t *base_table = ecs_get_table(world, ecs_get_alive(world, base));
    if (!base_table || !(base_table->flags & EcsTableIsPrefab)) {
        /* Don't instantiate children from base entities that aren't prefabs */
        return;
    }

    ecs_run_aperiodic(world, EcsAperiodicEmptyTables);
    
    ecs_id_record_t *idr = flecs_id_record_get(world, ecs_childof(base));
    ecs_table_cache_iter_t it;
    if (idr && flecs_table_cache_iter((ecs_table_cache_t*)idr, &it)) {
        const ecs_table_record_t *tr;
        while ((tr = flecs_table_cache_next(&it, ecs_table_record_t))) {
            instantiate_children(
                world, base, table, row, count, tr->hdr.table);
        }
    }
}

static
bool override_component(
    ecs_world_t *world,
    ecs_entity_t component,
    ecs_type_t type,
    ecs_table_t *table,
    ecs_table_t *other_table,
    const ecs_type_info_t *ti,
    ecs_column_t *column,
    int32_t row,
    int32_t count,
    bool notify_on_set);

static
bool override_from_base(
    ecs_world_t *world,
    ecs_entity_t base,
    ecs_entity_t component,
    ecs_table_t *table,
    ecs_table_t *other_table,
    const ecs_type_info_t *ti,
    ecs_column_t *column,
    int32_t row,
    int32_t count,
    bool notify_on_set)
{
    ecs_assert(component != 0, ECS_INTERNAL_ERROR, NULL);

    ecs_record_t *r = flecs_entities_get(world, base);
    ecs_table_t *base_table;
    if (!r || !(base_table = r->table)) {
        return false;
    }

    void *base_ptr = get_component(
        world, base_table, ECS_RECORD_TO_ROW(r->row), component);
    if (base_ptr) {
        int32_t index, data_size = ti->size;
        void *data_ptr = ecs_storage_get(column, data_size, row);

        ecs_copy_t copy = ti->hooks.copy;
        if (copy) {
            for (index = 0; index < count; index ++) {
                copy(data_ptr, base_ptr, 1, ti);
                data_ptr = ECS_OFFSET(data_ptr, data_size);
            }
        } else {
            for (index = 0; index < count; index ++) {
                ecs_os_memcpy(data_ptr, base_ptr, data_size);
                data_ptr = ECS_OFFSET(data_ptr, data_size);
            }                    
        }

        ecs_type_t ids = {
            .array = (ecs_id_t[]){ component },
            .count = 1
        };

        if (notify_on_set) {
            /* Check if the component was available for the previous table. If
             * the override is caused by an add operation, it does not introduce
             * a new component value, and the application should not be 
             * notified. 
             * 
             * If the override is the result if adding a IsA relationship
             * with an entity that has components with the OVERRIDE flag, an
             * event should be generated, since this represents a new component
             * (and component value) for the entity.
             * 
             * Note that this is an edge case, regular (self) triggers won't be 
             * notified because the event id is not the component but an IsA
             * relationship. Superset triggers will not be invoked because the
             * component is owned. */
            int32_t c = ecs_search_relation(world, other_table, 0, component, 
                EcsIsA, EcsUp, 0, 0, 0);
            if (c == -1) {
                flecs_notify(
                    world, table, other_table, row, count, EcsOnSet, &ids, 0);
            }
        }

        return true;
    } else {
        /* If component not found on base, check if base itself inherits */
        ecs_type_t base_type = base_table->type;
        return override_component(world, component, base_type, table, 
            other_table, ti, column, row, count, notify_on_set);
    }
}

static
bool override_component(
    ecs_world_t *world,
    ecs_entity_t component,
    ecs_type_t type,
    ecs_table_t *table,
    ecs_table_t *other_table,
    const ecs_type_info_t *ti,
    ecs_column_t *column,
    int32_t row,
    int32_t count,
    bool notify_on_set)
{
    ecs_entity_t *type_array = type.array;
    int32_t i, type_count = type.count;

    /* Walk prefabs */
    i = type_count - 1;
    do {
        ecs_entity_t e = type_array[i];

        if (!(e & ECS_ROLE_MASK)) {
            break;
        }

        if (ECS_HAS_RELATION(e, EcsIsA)) {
            if (override_from_base(world, ecs_pair_second(world, e), component,
                table, other_table, ti, column, row, count, notify_on_set))
            {
                return true;
            }
        }
    } while (--i >= 0);

    return false;
}

static
void components_override(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_table_t *other_table,
    int32_t row,
    int32_t count,
    ecs_type_t *added,
    bool notify_on_set)
{
    ecs_column_t *columns = table->data.columns;
    ecs_type_t type = table->type;
    ecs_table_t *storage_table = table->storage_table;

    int i;
    for (i = 0; i < added->count; i ++) {
        ecs_entity_t id = added->array[i];

        if (ECS_HAS_RELATION(id, EcsIsA)) {
            ecs_entity_t base = ECS_PAIR_SECOND(id);

            /* Cannot inherit from base if base is final */
            ecs_check(!ecs_has_id(world, ecs_get_alive(world, base), EcsFinal),
                ECS_CONSTRAINT_VIOLATED, NULL);
            ecs_check(base != 0, ECS_INVALID_PARAMETER, NULL);

            if (!world->stages[0].base) {
                /* Setting base prevents instantiating the hierarchy multiple
                 * times. The instantiate function recursively iterates the
                 * hierarchy to instantiate children. While this is happening,
                 * new tables are created which end up calling this function,
                 * which would call instantiate multiple times for the same
                 * level in the hierarchy. */
                world->stages[0].base = base;
                instantiate(world, base, table, row, count);
                world->stages[0].base = 0;
            }
        }

        if (!storage_table) {
            continue;
        }

        ecs_table_record_t *tr = flecs_table_record_get(
            world, storage_table, id);
        if (!tr) {
            continue;
        }

        ecs_id_record_t *idr = (ecs_id_record_t*)tr->hdr.cache;
        if (idr->flags & EcsIdDontInherit) {
            continue;
        }

        const ecs_type_info_t *ti = idr->type_info;
        if (!ti->size) {
            continue;
        }

        ecs_column_t *column = &columns[tr->column];
        override_component(world, id, type, table, other_table, ti, 
            column, row, count, notify_on_set);
    }
error:
    return;
}

static
void flecs_set_union(
    ecs_world_t *world,
    ecs_table_t *table,
    int32_t row,
    int32_t count,    
    ecs_type_t *ids,
    bool reset)
{
    ecs_id_t *array = ids->array;
    int32_t i, id_count = ids->count;

    for (i = 0; i < id_count; i ++) {
        ecs_id_t id = array[i];
        if (ECS_HAS_ROLE(id, PAIR)) {
            ecs_id_record_t *idr = flecs_id_record_get(world, 
                ecs_pair(EcsUnion, ECS_PAIR_FIRST(id)));
            if (!idr) {
                continue;
            }

            const ecs_table_record_t *tr = flecs_id_record_get_table(
                idr, table);
            ecs_assert(tr != NULL, ECS_INTERNAL_ERROR, NULL);
            int32_t column = tr->column - table->sw_offset;
            ecs_switch_t *sw = &table->data.sw_columns[column];
            ecs_entity_t union_case = 0;
            if (!reset) {
                union_case = ECS_PAIR_SECOND(id);
            }

            int32_t r;
            for (r = 0; r < count; r ++) {
                flecs_switch_set(sw, row + r, union_case);
            }
        }
    }
}

static
void flecs_add_remove_union(
    ecs_world_t *world,
    ecs_table_t *table,
    int32_t row,
    int32_t count,
    ecs_type_t *added,
    ecs_type_t *removed)
{
    if (added) {
        flecs_set_union(world, table, row, count, added, false);
    }
    if (removed) {
        flecs_set_union(world, table, row, count, removed, true);
    } 
}

static
ecs_record_t* new_entity(
    ecs_world_t *world,
    ecs_entity_t entity,
    ecs_record_t *record,
    ecs_table_t *new_table,
    ecs_table_diff_t *diff,
    bool construct,
    bool notify_on_set)
{
    int32_t new_row;

    if (!record) {
        record = flecs_entities_ensure(world, entity);
    }

    new_row = flecs_table_append(world, new_table, entity, record, construct);

    record->table = new_table;
    record->row = ECS_ROW_TO_RECORD(new_row, record->row & ECS_ROW_FLAGS_MASK);

    ecs_data_t *new_data = &new_table->data;
    ecs_assert(ecs_storage_count(&new_data[0].entities) > new_row, 
        ECS_INTERNAL_ERROR, NULL);
    (void)new_data;

    ecs_flags32_t flags = new_table->flags;

    if (flags & EcsTableHasAddActions) {
        flecs_notify_on_add(
            world, new_table, NULL, new_row, 1, diff, notify_on_set);       
    }

    return record;
}

static
void move_entity(
    ecs_world_t *world,
    ecs_entity_t entity,
    ecs_record_t *record,
    ecs_table_t *dst_table,
    ecs_table_diff_t *diff,
    bool construct,
    bool notify_on_set)
{
    ecs_table_t *src_table = record->table;
    int32_t src_row = ECS_RECORD_TO_ROW(record->row);
    
    ecs_assert(src_table != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(src_table != dst_table, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(src_row >= 0, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(ecs_storage_count(&src_table->data.entities) > src_row, 
        ECS_INTERNAL_ERROR, NULL);
    ecs_check(ecs_is_alive(world, entity), ECS_INVALID_PARAMETER, NULL);
    ecs_assert(record != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(record == flecs_entities_get(world, entity), ECS_INTERNAL_ERROR, NULL);

    int32_t dst_row = flecs_table_append(world, dst_table, entity, 
        record, false);

    /* Copy entity & components from src_table to dst_table */
    if (src_table->type.count) {
        flecs_notify_on_remove(world, src_table, dst_table, 
            ECS_RECORD_TO_ROW(src_row), 1, diff);

        flecs_table_move(world, entity, entity, dst_table, dst_row, 
            src_table, src_row, construct);                
    }

    /* Update entity index & delete old data after running remove actions */
    record->table = dst_table;
    record->row = ECS_ROW_TO_RECORD(dst_row, record->row & ECS_ROW_FLAGS_MASK);
    
    flecs_table_delete(world, src_table, src_row, false);

    /* If components were added, invoke add actions */
    ecs_flags32_t dst_flags = dst_table->flags;
    if (src_table != dst_table || diff->added.count) {
        if (diff->added.count && (dst_flags & EcsTableHasAddActions)) {
            flecs_notify_on_add(world, dst_table, src_table, dst_row, 1, diff, 
                notify_on_set);
        }
    }

error:
    return;
}

static
void delete_entity(
    ecs_world_t *world,
    ecs_record_t *record,
    ecs_table_diff_t *diff)
{    
    ecs_table_t *table = record->table;
    int32_t row = ECS_RECORD_TO_ROW(record->row);

    /* Invoke remove actions before deleting */
    if (table->flags & EcsTableHasRemoveActions) {   
        flecs_notify_on_remove(world, table, NULL, row, 1, diff);
    }

    flecs_table_delete(world, table, row, true);
}

/* Updating component monitors is a relatively expensive operation that only
 * happens for entities that are monitored. The approach balances the amount of
 * processing between the operation on the entity vs the amount of work that
 * needs to be done to rematch queries, as a simple brute force approach does
 * not scale when there are many tables / queries. Therefore we need to do a bit
 * of bookkeeping that is more intelligent than simply flipping a flag */
static
void update_component_monitor_w_array(
    ecs_world_t *world,
    ecs_type_t *ids)
{
    if (!ids) {
        return;
    }

    int i;
    for (i = 0; i < ids->count; i ++) {
        ecs_entity_t id = ids->array[i];
        if (ECS_HAS_ROLE(id, PAIR)) {
            flecs_monitor_mark_dirty(world, 
                ecs_pair(ECS_PAIR_FIRST(id), EcsWildcard));
        }

        flecs_monitor_mark_dirty(world, id);
    }
}

static
void update_component_monitors(
    ecs_world_t *world,
    ecs_type_t *added,
    ecs_type_t *removed)
{
    update_component_monitor_w_array(world, added);
    update_component_monitor_w_array(world, removed);
}

static
void flecs_commit(
    ecs_world_t *world,
    ecs_entity_t entity,
    ecs_record_t *record,
    ecs_table_t *dst_table,   
    ecs_table_diff_t *diff,
    bool construct,
    bool notify_on_set)
{
    ecs_assert(!(world->flags & EcsWorldReadonly), ECS_INTERNAL_ERROR, NULL);
    
    ecs_table_t *src_table = record ? record->table : NULL;
    if (src_table == dst_table) {
        /* If source and destination table are the same no action is needed *
         * However, if a component was added in the process of traversing a
         * table, this suggests that a case switch could have occured. */
        if (((diff->added.count) || (diff->removed.count)) && 
             src_table && src_table->flags & EcsTableHasUnion) 
        {
            flecs_add_remove_union(world, src_table, 
                ECS_RECORD_TO_ROW(record->row), 1,
                &diff->added, &diff->removed);
        }

        return;
    }

    if (src_table) {
        ecs_assert(dst_table != NULL, ECS_INTERNAL_ERROR, NULL);

        if (dst_table->type.count) { 
            move_entity(world, entity, record, dst_table, diff, 
                construct, notify_on_set);
        } else {
            delete_entity(world, record, diff);
            record->table = NULL;
        }      
    } else {        
        if (dst_table->type.count) {
            record = new_entity(world, entity, record, dst_table, diff, 
                construct, notify_on_set);
        }        
    }

    /* If the entity is being watched, it is being monitored for changes and
     * requires rematching systems when components are added or removed. This
     * ensures that systems that rely on components from containers or prefabs
     * update the matched tables when the application adds or removes a 
     * component from, for example, a container. */
    if (record->row & ECS_ROW_FLAGS_MASK) {
        update_component_monitors(world, &diff->added, &diff->removed);
    }

    if ((!src_table || !src_table->type.count) && world->range_check_enabled) {
        ecs_check(!world->info.max_id || entity <= world->info.max_id, 
            ECS_OUT_OF_RANGE, 0);
        ecs_check(entity >= world->info.min_id, 
            ECS_OUT_OF_RANGE, 0);
    } 
error:
    return;
}

static
void new(
    ecs_world_t *world,
    ecs_entity_t entity,
    ecs_type_t *to_add)
{
    int32_t i, count = to_add->count;
    ecs_table_t *table = &world->store.root;
    
    ecs_diff_buffer_t diff = ECS_DIFF_INIT;
    for (i = 0; i < count; i ++) {
        table = table_append(world, table, to_add->array[i], &diff);
    }

    ecs_table_diff_t table_diff = diff_to_table_diff(&diff);
    new_entity(world, entity, NULL, table, &table_diff, true, true);
    diff_free(&diff);
}

static
const ecs_entity_t* new_w_data(
    ecs_world_t *world,
    ecs_table_t *table,
    const ecs_entity_t *entities,
    ecs_type_t *component_ids,
    int32_t count,
    void **component_data,
    bool is_move,
    int32_t *row_out,
    ecs_table_diff_t *diff)
{
    int32_t sparse_count = 0;
    if (!entities) {
        sparse_count = flecs_entities_count(world);
        entities = flecs_sparse_new_ids(ecs_eis(world), count);
    }

    if (!table) {
        return entities;
    }

    ecs_type_t type = table->type;   
    if (!type.count) {
        return entities;        
    }

    ecs_type_t component_array = { 0 };
    if (!component_ids) {
        component_ids = &component_array;
        component_array.array = type.array;
        component_array.count = type.count;
    }

    ecs_data_t *data = &table->data;
    int32_t row = flecs_table_appendn(world, table, data, count, entities);
    
    /* Update entity index. */
    int i;
    ecs_record_t **records = ecs_storage_first(&data->records);
    for (i = 0; i < count; i ++) { 
        records[row + i] = flecs_entities_set(world, entities[i], 
            &(ecs_record_t){
                .table = table,
                .row = ECS_ROW_TO_RECORD(row + i, 0)
            });
    }

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

    flecs_notify_on_add(world, table, NULL, row, count, diff, 
        component_data == NULL);

    if (component_data) {
        /* Set components that we're setting in the component mask so the init
         * actions won't call OnSet triggers for them. This ensures we won't
         * call OnSet triggers multiple times for the same component */
        int32_t c_i;
        ecs_table_t *storage_table = table->storage_table;
        for (c_i = 0; c_i < component_ids->count; c_i ++) {
            void *src_ptr = component_data[c_i];
            if (!src_ptr) {
                continue;
            }

            /* Find component in storage type */
            ecs_entity_t id = component_ids->array[c_i];
            const ecs_table_record_t *tr = flecs_table_record_get(
                world, storage_table, id);
            ecs_assert(tr != NULL, ECS_INTERNAL_ERROR, NULL);
            ecs_assert(tr->count == 1, ECS_INTERNAL_ERROR, NULL);

            int32_t index = tr->column;
            ecs_type_info_t *ti = table->type_info[index];
            ecs_column_t *column = &table->data.columns[index];
            int32_t size = ti->size;
            ecs_assert(size != 0, ECS_INTERNAL_ERROR, NULL);
            void *ptr = ecs_storage_get(column, size, row);

            ecs_copy_t copy;
            ecs_move_t move;
            if (is_move && (move = ti->hooks.move)) {
                move(ptr, src_ptr, count, ti);
            } else if (!is_move && (copy = ti->hooks.copy)) {
                copy(ptr, src_ptr, count, ti);
            } else {
                ecs_os_memcpy(ptr, src_ptr, size * count);
            } 
        };

        flecs_notify_on_set(world, table, row, count, NULL, true);
        flecs_notify_on_set(world, table, row, count, &diff->on_set, false);
    }

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

    if (row_out) {
        *row_out = row;
    }

    if (sparse_count) {
        entities = flecs_sparse_ids(ecs_eis(world));
        return &entities[sparse_count];
    } else {
        return entities;
    }
}

static
void add_id_w_record(
    ecs_world_t *world,
    ecs_entity_t entity,
    ecs_record_t *record,
    ecs_id_t id,
    bool construct)
{
    ecs_table_diff_t diff;

    ecs_table_t *src_table = NULL;
    if (record) {
        src_table = record->table;
    }

    ecs_table_t *dst_table = flecs_table_traverse_add(
        world, src_table, &id, &diff);

    flecs_commit(world, entity, record, dst_table, &diff, construct, 
        false); /* notify_on_set = false, this function is only called from
                 * functions that are about to set the component. */
}

static
void add_id(
    ecs_world_t *world,
    ecs_entity_t entity,
    ecs_id_t id)
{
    ecs_stage_t *stage = flecs_stage_from_world(&world);

    if (flecs_defer_add(world, stage, entity, id)) {
        return;
    }

    ecs_record_t *r = flecs_entities_ensure(world, entity);
    ecs_table_diff_t diff;
    ecs_table_t *src_table = r->table;
    ecs_table_t *dst_table = flecs_table_traverse_add(
        world, src_table, &id, &diff);

    flecs_commit(world, entity, r, dst_table, &diff, true, true);

    flecs_defer_end(world, stage);
}

static
void remove_id(
    ecs_world_t *world,
    ecs_entity_t entity,
    ecs_id_t id)
{
    ecs_stage_t *stage = flecs_stage_from_world(&world);

    if (flecs_defer_remove(world, stage, entity, id)) {
        return;
    }

    ecs_record_t *r = flecs_entities_get(world, entity);
    ecs_table_t *src_table = NULL;
    if (!r || !(src_table = r->table)) {
        goto done; /* Nothing to remove */
    }

    ecs_table_diff_t diff;
    ecs_table_t *dst_table = flecs_table_traverse_remove(
        world, src_table, &id, &diff);

    flecs_commit(world, entity, r, dst_table, &diff, true, true);

done:
    flecs_defer_end(world, stage);
}

static
void *get_mutable(
    ecs_world_t *world,
    ecs_entity_t entity,
    ecs_entity_t id,
    ecs_record_t *r)
{
    void *dst = NULL;

    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(id != 0, ECS_INVALID_PARAMETER, NULL);
    ecs_check(r != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check((id & ECS_COMPONENT_MASK) == id || 
        ECS_HAS_ROLE(id, PAIR), ECS_INVALID_PARAMETER, NULL);

    if (r->table) {
        dst = get_component(world, r->table, ECS_RECORD_TO_ROW(r->row), id);
    }

    if (!dst) {
        /* If entity didn't have component yet, add it */
        add_id_w_record(world, entity, r, id, true);

        /* Flush commands so the pointer we're fetching is stable */
        ecs_defer_end(world);
        ecs_defer_begin(world);

        ecs_assert(r->table != NULL, ECS_INTERNAL_ERROR, NULL);
        ecs_assert(r->table->storage_table != NULL, ECS_INTERNAL_ERROR, NULL);
        dst = get_component(world, r->table, ECS_RECORD_TO_ROW(r->row), id);

        return dst;
    }

error:
    return dst;
}

/* -- Private functions -- */
static
void flecs_notify_on_add(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_table_t *other_table,
    int32_t row,
    int32_t count,
    ecs_table_diff_t *diff,
    bool run_on_set)
{
    ecs_assert(diff != NULL, ECS_INTERNAL_ERROR, NULL);

    if (diff->added.count) {
        if (table->flags & EcsTableHasIsA) {
            components_override(world, table, other_table, row, count, 
                &diff->added, run_on_set);
        }

        if (table->flags & EcsTableHasUnion) {
            flecs_add_remove_union(world, table, row, count, &diff->added, NULL);
        }

        if (table->flags & EcsTableHasOnAdd) {
            flecs_notify(world, table, other_table, row, count, EcsOnAdd, 
                &diff->added, 0);
        }
    }

    /* When a IsA relationship is added to an entity, that entity inherits the
     * components from the base. Send OnSet notifications so that an application
     * can respond to these new components. */
    if (run_on_set && diff->on_set.count) {
        flecs_notify(world, table, other_table, row, count, EcsOnSet, &diff->on_set, 
            EcsIsA);
    }
}

void flecs_notify_on_remove(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_table_t *other_table,
    int32_t row,
    int32_t count,
    ecs_table_diff_t *diff)
{
    ecs_assert(diff != NULL, ECS_INTERNAL_ERROR, NULL);

    if (count) {
        if (diff->un_set.count) {
            flecs_notify(world, table, other_table, row, count, EcsUnSet, &diff->un_set, 0);
        }

        if (table->flags & EcsTableHasOnRemove && diff->removed.count) {
            flecs_notify(world, table, other_table, row, count, EcsOnRemove, 
                &diff->removed, 0);
        }

        if (table->flags & EcsTableHasIsA && diff->on_set.count) {
            flecs_notify(world, table, other_table, row, count, EcsOnSet, 
                &diff->on_set, 0);
        }
    }
}

void flecs_notify_on_set(
    ecs_world_t *world,
    ecs_table_t *table,
    int32_t row,
    int32_t count,
    ecs_type_t *ids,
    bool owned)
{
    ecs_data_t *data = &table->data;

    ecs_entity_t *entities = ecs_storage_get_t(
        &data->entities, ecs_entity_t, row);
    ecs_assert(entities != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert((row + count) <= ecs_storage_count(&data->entities), 
        ECS_INTERNAL_ERROR, NULL);

    ecs_type_t local_ids;
    if (!ids) {
        local_ids.array = table->storage_ids;
        local_ids.count = table->storage_count;
        ids = &local_ids;
    }

    if (owned) {
        ecs_table_t *storage_table = table->storage_table;
        int i;
        for (i = 0; i < ids->count; i ++) {
            ecs_id_t id = ids->array[i];
            const ecs_table_record_t *tr = flecs_table_record_get(world, 
                storage_table, id);
            ecs_assert(tr != NULL, ECS_INTERNAL_ERROR, NULL);
            ecs_assert(tr->count == 1, ECS_INTERNAL_ERROR, NULL);
            int32_t column = tr->column;
            const ecs_type_info_t *ti = table->type_info[column];
            ecs_iter_action_t on_set = ti->hooks.on_set;
            if (on_set) {
                ecs_column_t *c = &table->data.columns[column];
                ecs_size_t size = ti->size;
                void *ptr = ecs_storage_get(c, size, row);
                ecs_assert(size != 0, ECS_INTERNAL_ERROR, NULL);

                ecs_iter_t it = {.term_count = 1};
                it.entities = entities;
                
                flecs_iter_init(&it, flecs_iter_cache_all);
                it.world = world;
                it.real_world = world;
                it.table = table;
                it.ptrs[0] = ptr;
                it.sizes[0] = size;
                it.ids[0] = id;
                it.event = EcsOnSet;
                it.event_id = id;
                it.ctx = ti->hooks.ctx;
                it.binding_ctx = ti->hooks.binding_ctx;
                it.count = count;
                flecs_iter_validate(&it);
                on_set(&it);
            }
        }
    }

    /* Run OnSet notifications */
    if (table->flags & EcsTableHasOnSet && ids->count) {
        flecs_notify(world, table, NULL, row, count, EcsOnSet, ids, 0);
    }
}

uint32_t flecs_record_to_row(
    uint32_t row, 
    bool *is_watched_out) 
{
    *is_watched_out = (row & ECS_ROW_FLAGS_MASK) != 0;
    return row & ECS_ROW_MASK;
}

uint32_t flecs_row_to_record(
    uint32_t row, 
    bool is_watched) 
{
    return row | (EcsEntityObserved * is_watched);
}

void flecs_add_flag(
    ecs_world_t *world,
    ecs_entity_t entity,
    uint32_t flag)
{
    ecs_record_t *record = flecs_entities_get(world, entity);
    if (!record) {
        ecs_record_t new_record = {.row = flag, .table = NULL};
        flecs_entities_set(world, entity, &new_record);
    } else {
        record->row |= flag;
    }
}


/* -- Public functions -- */

bool ecs_commit(
    ecs_world_t *world,
    ecs_entity_t entity,
    ecs_record_t *record,
    ecs_table_t *table,
    const ecs_type_t *added,
    const ecs_type_t *removed)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);

    ecs_table_t *src_table = NULL;
    if (!record) {
        record = flecs_entities_get(world, entity);
        src_table = record->table;
    }

    ecs_table_diff_t diff;
    ecs_os_zeromem(&diff);

    if (added) {
        diff.added = *added;
    }
    if (removed) {
        diff.added = *removed;
    }
    
    flecs_commit(world, entity, record, table, &diff, true, true);

    return src_table != table;
error:
    return false;
}

ecs_entity_t ecs_set_with(
    ecs_world_t *world,
    ecs_id_t id)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_stage_t *stage = flecs_stage_from_world(&world);
    ecs_id_t prev = stage->with;
    stage->with = id;
    return prev;
error:
    return 0;
}

ecs_id_t ecs_get_with(
    const ecs_world_t *world)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    const ecs_stage_t *stage = flecs_stage_from_readonly_world(world);
    return stage->with;
error:
    return 0;
}

ecs_entity_t ecs_new_id(
    ecs_world_t *world)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);

    const ecs_stage_t *stage = flecs_stage_from_readonly_world(world);

    /* It is possible that the world passed to this function is a stage, so
     * make sure we have the actual world. Cast away const since this is one of
     * the few functions that may modify the world while it is in readonly mode,
     * since it is thread safe (uses atomic inc when in threading mode) */
    ecs_world_t *unsafe_world = (ecs_world_t*)ecs_get_world(world);

    ecs_entity_t entity;
    int32_t stage_count = ecs_get_stage_count(unsafe_world);
    if (stage->asynchronous || (ecs_os_has_threading() && stage_count > 1)) {
        /* Can't atomically increase number above max int */
        ecs_assert(unsafe_world->info.last_id < UINT_MAX, 
            ECS_INVALID_OPERATION, NULL);
        entity = (ecs_entity_t)ecs_os_ainc(
            (int32_t*)&unsafe_world->info.last_id);
    } else {
        entity = flecs_entities_recycle(unsafe_world);
    }

    ecs_assert(!unsafe_world->info.max_id || 
        ecs_entity_t_lo(entity) <= unsafe_world->info.max_id, 
        ECS_OUT_OF_RANGE, NULL);

    return entity;
error:
    return 0;
}

ecs_entity_t ecs_new_low_id(
    ecs_world_t *world)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);

    /* It is possible that the world passed to this function is a stage, so
     * make sure we have the actual world. Cast away const since this is one of
     * the few functions that may modify the world while it is in readonly mode,
     * but only if single threaded. */
    ecs_world_t *unsafe_world = (ecs_world_t*)ecs_get_world(world);
    if (unsafe_world->flags & EcsWorldReadonly) {
        /* Can't issue new comp id while iterating when in multithreaded mode */
        ecs_check(ecs_get_stage_count(world) <= 1,
            ECS_INVALID_WHILE_READONLY, NULL);
    }

    ecs_entity_t id = 0;
    if (unsafe_world->info.last_component_id < ECS_HI_COMPONENT_ID) {
        do {
            id = unsafe_world->info.last_component_id ++;
        } while (ecs_exists(unsafe_world, id) && id <= ECS_HI_COMPONENT_ID);        
    }

    if (!id || id >= ECS_HI_COMPONENT_ID) {
        /* If the low component ids are depleted, return a regular entity id */
        id = ecs_new_id(unsafe_world);
    }

    return id;
error: 
    return 0;
}

ecs_entity_t ecs_new_w_id(
    ecs_world_t *world,
    ecs_id_t id)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(!id || ecs_id_is_valid(world, id), ECS_INVALID_PARAMETER, NULL);

    ecs_stage_t *stage = flecs_stage_from_world(&world);    
    ecs_entity_t entity = ecs_new_id(world);

    ecs_id_t ids[3];
    ecs_type_t to_add = { .array = ids, .count = 0 };

    if (id) {
        ids[to_add.count ++] = id;
    }

    ecs_id_t with = stage->with;
    if (with) {
        ids[to_add.count ++] = with;
    }

    ecs_entity_t scope = stage->scope;
    if (scope) {
        if (!id || !ECS_HAS_RELATION(id, EcsChildOf)) {
            ids[to_add.count ++] = ecs_pair(EcsChildOf, scope);
        }
    }
    if (to_add.count) {
        if (flecs_defer_new(world, stage, entity, to_add.array[0])) {
            int i;
            for (i = 1; i < to_add.count; i ++) {
                flecs_defer_add(world, stage, entity, to_add.array[i]);
            }
            return entity;
        }

        new(world, entity, &to_add);
    } else {
        if (flecs_defer_new(world, stage, entity, 0)) {
            return entity;
        }

        flecs_entities_set(world, entity, &(ecs_record_t){ 0 });
    }
    flecs_defer_end(world, stage);

    return entity;
error:
    return 0;
}

#ifdef FLECS_PARSER

/* Traverse table graph by either adding or removing identifiers parsed from the
 * passed in expression. */
static
ecs_table_t *traverse_from_expr(
    ecs_world_t *world,
    ecs_table_t *table,
    const char *name,
    const char *expr,
    ecs_diff_buffer_t *diff,
    bool replace_and,
    bool *error)
{
    const char *ptr = expr;
    if (ptr) {
        ecs_term_t term = {0};
        while (ptr[0] && (ptr = ecs_parse_term(world, name, expr, ptr, &term))){
            if (!ecs_term_is_initialized(&term)) {
                break;
            }

            if (!(term.first.flags & (EcsSelf|EcsUp))) {
                term.first.flags = EcsSelf;
            }
            if (!(term.second.flags & (EcsSelf|EcsUp))) {
                term.second.flags = EcsSelf;
            }
            if (!(term.src.flags & (EcsSelf|EcsUp))) {
                term.src.flags = EcsSelf;
            }

            if (ecs_term_finalize(world, &term)) {
                ecs_term_fini(&term);
                if (error) {
                    *error = true;
                }
                return NULL;
            }

            if (!ecs_id_is_valid(world, term.id)) {
                ecs_term_fini(&term);
                ecs_parser_error(name, expr, (ptr - expr), 
                    "invalid term for add expression");
                return NULL;
            }

            if (term.oper == EcsAnd || !replace_and) {
                /* Regular AND expression */
                table = table_append(world, table, term.id, diff);
            }

            ecs_term_fini(&term);
        }

        if (!ptr) {
            if (error) {
                *error = true;
            }
            return NULL;
        }
    }

    return table;
}

/* Add/remove components based on the parsed expression. This operation is 
 * slower than traverse_from_expr, but safe to use from a deferred context. */
static
void defer_from_expr(
    ecs_world_t *world,
    ecs_entity_t entity,
    const char *name,
    const char *expr,
    bool is_add,
    bool replace_and)
{
    const char *ptr = expr;
    if (ptr) {
        ecs_term_t term = {0};
        while (ptr[0] && (ptr = ecs_parse_term(world, name, expr, ptr, &term))){
            if (!ecs_term_is_initialized(&term)) {
                break;
            }

            if (ecs_term_finalize(world, &term)) {
                return;
            }

            if (!ecs_id_is_valid(world, term.id)) {
                ecs_term_fini(&term);
                ecs_parser_error(name, expr, (ptr - expr), 
                    "invalid term for add expression");
                return;
            }

            if (term.oper == EcsAnd || !replace_and) {
                /* Regular AND expression */
                if (is_add) {
                    ecs_add_id(world, entity, term.id);
                } else {
                    ecs_remove_id(world, entity, term.id);
                }
            }

            ecs_term_fini(&term);
        }
    }
}
#endif

/* If operation is not deferred, add components by finding the target
 * table and moving the entity towards it. */
static 
int traverse_add(
    ecs_world_t *world,
    ecs_entity_t result,
    const char *name,
    const ecs_entity_desc_t *desc,
    ecs_entity_t scope,
    ecs_id_t with,
    bool new_entity,
    bool name_assigned)
{
    const char *sep = desc->sep;
    const char *root_sep = desc->root_sep;

    /* Find existing table */
    ecs_table_t *src_table = NULL, *table = NULL;
    ecs_record_t *r = NULL;
    if (!new_entity) {
        r = flecs_entities_get(world, result);
        if (r) {
            table = r->table;
        }
    }

    /* Find destination table */
    ecs_diff_buffer_t diff = ECS_DIFF_INIT;

    /* If this is a new entity without a name, add the scope. If a name is
     * provided, the scope will be added by the add_path_w_sep function */
    if (new_entity) {
        if (new_entity && scope && !name && !name_assigned) {
            table = table_append(
                world, table, ecs_pair(EcsChildOf, scope), &diff);
        }
        if (with) {
            table = table_append(world, table, with, &diff);
        }
    }

    /* If a name is provided but not yet assigned, add the Name component */
    if (name && !name_assigned) {
        table = table_append(world, table, 
            ecs_pair(ecs_id(EcsIdentifier), EcsName), &diff);
    }

    /* Add components from the 'add' id array */
    int32_t i = 0;
    ecs_id_t id;
    const ecs_id_t *ids = desc->add;
    while ((i < ECS_ID_CACHE_SIZE) && (id = ids[i ++])) {
        bool should_add = true;
        if (ECS_HAS_ROLE(id, PAIR) && ECS_PAIR_FIRST(id) == EcsChildOf) {
            scope = ECS_PAIR_SECOND(id);
            if ((!desc->id && desc->name) || (name && !name_assigned)) {
                /* If name is added to entity, pass scope to add_path instead
                 * of adding it to the table. The provided name may have nested
                 * elements, in which case the parent provided here is not the
                 * parent the entity will end up with. */
                should_add = false;
            }
        }
        if (should_add) {
            table = table_append(world, table, id, &diff);
        }
    }

    /* Add components from the 'add_expr' expression */
    if (desc->add_expr && ecs_os_strcmp(desc->add_expr, "0")) {
#ifdef FLECS_PARSER
        bool error = false;
        table = traverse_from_expr(
            world, table, name, desc->add_expr, &diff, true, &error);
        if (error) {
            return -1;
        }
#else
        ecs_abort(ECS_UNSUPPORTED, "parser addon is not available");
#endif
    }

    /* Commit entity to destination table */
    if (src_table != table) {
        ecs_defer_begin(world);
        ecs_table_diff_t table_diff = diff_to_table_diff(&diff);
        flecs_commit(world, result, r, table, &table_diff, true, true);
        ecs_defer_end(world);
    }

    /* Set name */
    if (name && !name_assigned) {
        ecs_add_path_w_sep(world, result, scope, name, sep, root_sep);
        ecs_assert(ecs_get_name(world, result) != NULL,
            ECS_INTERNAL_ERROR, NULL);
    }

    if (desc->symbol && desc->symbol[0]) {
        const char *sym = ecs_get_symbol(world, result);
        if (sym) {
            ecs_assert(!ecs_os_strcmp(desc->symbol, sym),
                ECS_INCONSISTENT_NAME, desc->symbol);
        } else {
            ecs_set_symbol(world, result, desc->symbol);
        }
    }

    diff_free(&diff);

    return 0;
}

/* When in deferred mode, we need to add/remove components one by one using
 * the regular operations. */
static 
void deferred_add_remove(
    ecs_world_t *world,
    ecs_entity_t entity,
    const char *name,
    const ecs_entity_desc_t *desc,
    ecs_entity_t scope,
    ecs_id_t with,
    bool new_entity,
    bool name_assigned)
{
    const char *sep = desc->sep;
    const char *root_sep = desc->root_sep;

    /* If this is a new entity without a name, add the scope. If a name is
     * provided, the scope will be added by the add_path_w_sep function */
    if (new_entity) {
        if (new_entity && scope && !name && !name_assigned) {
            ecs_add_id(world, entity, ecs_pair(EcsChildOf, scope));
        }

        if (with) {
            ecs_add_id(world, entity, with);
        }
    }

    /* Add components from the 'add' id array */
    int32_t i = 0;
    ecs_id_t id;
    const ecs_id_t *ids = desc->add;
    while ((i < ECS_ID_CACHE_SIZE) && (id = ids[i ++])) {
        bool defer = true;
        if (ECS_HAS_ROLE(id, PAIR) && ECS_PAIR_FIRST(id) == EcsChildOf) {
            scope = ECS_PAIR_SECOND(id);
            if (!desc->id || (name && !name_assigned)) {
                /* New named entities are created by temporarily going out of
                 * readonly mode to ensure no duplicates are created. */
                defer = false;
            }
        }
        if (defer) {
            ecs_add_id(world, entity, id);
        }
    }

    /* Add components from the 'add_expr' expression */
    if (desc->add_expr) {
#ifdef FLECS_PARSER
        defer_from_expr(world, entity, name, desc->add_expr, true, true);
#else
        ecs_abort(ECS_UNSUPPORTED, "parser addon is not available");
#endif
    }

    int32_t thread_count = ecs_get_stage_count(world);

    /* Set name */
    if (name && !name_assigned) {
        /* To prevent creating two entities with the same name, temporarily go
         * out of readonly mode if it's safe to do so. */
        ecs_suspend_readonly_state_t state;
        if (thread_count <= 1) {
            /* When not running on multiple threads we can temporarily leave
             * readonly mode which ensures that we don't accidentally create
             * two entities with the same name. */
            ecs_world_t *real_world = flecs_suspend_readonly(world, &state);
            ecs_add_path_w_sep(real_world, entity, scope, name, sep, root_sep);
            flecs_resume_readonly(real_world, &state);
        } else {
            /* In multithreaded mode we can't leave readonly mode, which means
             * there is a risk of creating two entities with the same name. 
             * Future improvements will be able to detect this. */
            ecs_add_path_w_sep(world, entity, scope, name, sep, root_sep);
        }
    }

    /* Set symbol */
    if (desc->symbol) {
        const char *sym = ecs_get_symbol(world, entity);
        if (!sym || ecs_os_strcmp(sym, desc->symbol)) {
            if (thread_count <= 1) { /* See above */
                ecs_suspend_readonly_state_t state;
                ecs_world_t *real_world = flecs_suspend_readonly(world, &state);
                ecs_set_symbol(world, entity, desc->symbol);
                flecs_resume_readonly(real_world, &state);
            } else {
                ecs_set_symbol(world, entity, desc->symbol);
            }
        }
    }
}

ecs_entity_t ecs_entity_init(
    ecs_world_t *world,
    const ecs_entity_desc_t *desc)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(desc != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(desc->_canary == 0, ECS_INVALID_PARAMETER, NULL);

    ecs_stage_t *stage = flecs_stage_from_world(&world);
    ecs_entity_t scope = stage->scope;
    ecs_id_t with = ecs_get_with(world);

    const char *name = desc->name;
    const char *sep = desc->sep;
    if (!sep) {
        sep = ".";
    }

    if (name && !name[0]) {
        name = NULL;
    }

    const char *root_sep = desc->root_sep;
    bool new_entity = false;
    bool name_assigned = false;

    /* Remove optional prefix from name. Entity names can be derived from 
     * language identifiers, such as components (typenames) and systems
     * function names). Because C does not have namespaces, such identifiers
     * often encode the namespace as a prefix.
     * To ensure interoperability between C and C++ (and potentially other 
     * languages with namespacing) the entity must be stored without this prefix
     * and with the proper namespace, which is what the name_prefix is for */
    const char *prefix = world->name_prefix;
    if (name && prefix) {
        ecs_size_t len = ecs_os_strlen(prefix);
        if (!ecs_os_strncmp(name, prefix, len) && 
           (isupper(name[len]) || name[len] == '_')) 
        {
            if (name[len] == '_') {
                name = name + len + 1;
            } else {
                name = name + len;
            }
        }
    }

    /* Find or create entity */
    ecs_entity_t result = desc->id;
    if (!result) {
        if (name) {
            /* If add array contains a ChildOf pair, use it as scope instead */
            const ecs_id_t *ids = desc->add;
            ecs_id_t id;
            int32_t i = 0;
            while ((i < ECS_ID_CACHE_SIZE) && (id = ids[i ++])) {
                if (ECS_HAS_ROLE(id, PAIR) && 
                    (ECS_PAIR_FIRST(id) == EcsChildOf))
                {
                    scope = ECS_PAIR_SECOND(id);
                }
            }

            result = ecs_lookup_path_w_sep(
                world, scope, name, sep, root_sep, false);
            if (result) {
                name_assigned = true;
            }
        }

        if (!result) {
            if (desc->use_low_id) {
                result = ecs_new_low_id(world);
            } else {
                result = ecs_new_id(world);
            }
            new_entity = true;
            ecs_assert(ecs_get_type(world, result) == NULL,
                ECS_INTERNAL_ERROR, NULL);
        }
    } else {
        /* Make sure provided id is either alive or revivable */
        ecs_ensure(world, result);

        name_assigned = ecs_has_pair(
            world, result, ecs_id(EcsIdentifier), EcsName);
        if (name && name_assigned) {
            /* If entity has name, verify that name matches */
            char *path = ecs_get_path_w_sep(world, scope, result, sep, NULL);
            if (path) {
                if (ecs_os_strcmp(path, name)) {
                    /* Mismatching name */
                    ecs_os_free(path);
                    return 0;
                }
                ecs_os_free(path);
            }
        }
    }

    ecs_assert(name_assigned == ecs_has_pair(
        world, result, ecs_id(EcsIdentifier), EcsName),
            ECS_INTERNAL_ERROR, NULL);

    if (stage->defer) {
        deferred_add_remove((ecs_world_t*)stage, result, name, desc, 
            scope, with, new_entity, name_assigned);
    } else {
        if (traverse_add(world, result, name, desc,
            scope, with, new_entity, name_assigned)) 
        {
            return 0;
        }
    }

    return result;
error:
    return 0;
}

const ecs_entity_t* ecs_bulk_init(
    ecs_world_t *world,
    const ecs_bulk_desc_t *desc)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(desc != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(desc->_canary == 0, ECS_INVALID_PARAMETER, NULL);

    const ecs_entity_t *entities = desc->entities;
    int32_t count = desc->count;

    int32_t sparse_count = 0;
    if (!entities) {
        sparse_count = flecs_entities_count(world);
        entities = flecs_sparse_new_ids(ecs_eis(world), count);
        ecs_assert(entities != NULL, ECS_INTERNAL_ERROR, NULL);
    } else {
        int i;
        for (i = 0; i < count; i ++) {
            ecs_ensure(world, entities[i]);
        }
    }

    ecs_type_t ids;
    ecs_table_t *table = desc->table;
    ecs_diff_buffer_t diff = ECS_DIFF_INIT;
    if (!table) {
        int32_t i = 0;
        ecs_id_t id;
        while ((id = desc->ids[i])) {
            table = table_append(world, table, id, &diff);
            i ++;
        }

        ids.array = (ecs_id_t*)desc->ids;
        ids.count = i;
    } else {
        diff.added.type.array = table->type.array;
        diff.added.type.count = table->type.count;
        ids = diff.added.type;
    }

    ecs_table_diff_t table_diff = diff_to_table_diff(&diff);
    new_w_data(world, table, entities, &ids, count, desc->data, true, NULL, 
        &table_diff);

    if (!sparse_count) {
        return entities;
    } else {
        /* Refetch entity ids, in case the underlying array was reallocated */
        entities = flecs_sparse_ids(ecs_eis(world));
        return &entities[sparse_count];
    }
error:
    return NULL;
}

ecs_entity_t ecs_component_init(
    ecs_world_t *world,
    const ecs_component_desc_t *desc)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(desc != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(desc->_canary == 0, ECS_INVALID_PARAMETER, NULL);

    ecs_suspend_readonly_state_t readonly_state;
    world = flecs_suspend_readonly(world, &readonly_state);

    ecs_entity_t result = desc->entity;
    if (!result) {
        result = ecs_new_low_id(world);
    }

    EcsComponent *ptr = ecs_get_mut(world, result, EcsComponent);
    if (!ptr->size) {
        ecs_assert(ptr->alignment == 0, ECS_INTERNAL_ERROR, NULL);
        ptr->size = desc->type.size;
        ptr->alignment = desc->type.alignment;
        if (!ptr->size) {
            ecs_trace("#[green]tag#[reset] %s created", 
                ecs_get_name(world, result));
        } else {
            ecs_trace("#[green]component#[reset] %s created", 
                ecs_get_name(world, result));
        }
    } else {
        if (ptr->size != desc->type.size) {
            char *path = ecs_get_fullpath(world, result);
            ecs_abort(ECS_INVALID_COMPONENT_SIZE, path);
            ecs_os_free(path);
        }
        if (ptr->alignment != desc->type.alignment) {
            char *path = ecs_get_fullpath(world, result);
            ecs_abort(ECS_INVALID_COMPONENT_ALIGNMENT, path);
            ecs_os_free(path);
        }
    }

    ecs_modified(world, result, EcsComponent);

    if (desc->type.size && 
        !ecs_id_in_use(world, result) && 
        !ecs_id_in_use(world, ecs_pair(result, EcsWildcard)))
    {
        ecs_set_hooks_id(world, result, &desc->type.hooks);
    }

    if (result >= world->info.last_component_id && result < ECS_HI_COMPONENT_ID) {
        world->info.last_component_id = result + 1;
    }

    /* Ensure components cannot be deleted */
    ecs_add_pair(world, result, EcsOnDelete, EcsPanic);

    flecs_resume_readonly(world, &readonly_state);
    
    ecs_assert(result != 0, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(ecs_has(world, result, EcsComponent), ECS_INTERNAL_ERROR, NULL);

    return result;
error:
    return 0;
}

const ecs_entity_t* ecs_bulk_new_w_id(
    ecs_world_t *world,
    ecs_id_t id,
    int32_t count)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_stage_t *stage = flecs_stage_from_world(&world);

    const ecs_entity_t *ids;
    if (flecs_defer_bulk_new(world, stage, count, id, &ids)) {
        return ids;
    }

    ecs_table_t *table = &world->store.root;
    ecs_diff_buffer_t diff = ECS_DIFF_INIT;
    
    if (id) {
        table = table_append(world, table, id, &diff);
    }

    ecs_table_diff_t td = diff_to_table_diff(&diff);
    ids = new_w_data(world, table, NULL, NULL, count, NULL, false, NULL, &td);
    flecs_defer_end(world, stage);

    return ids;
error:
    return NULL;
}

void ecs_clear(
    ecs_world_t *world,
    ecs_entity_t entity)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(ecs_is_valid(world, entity), ECS_INVALID_PARAMETER, NULL);

    ecs_stage_t *stage = flecs_stage_from_world(&world);
    if (flecs_defer_clear(world, stage, entity)) {
        return;
    }

    ecs_record_t *r = flecs_entities_get(world, entity);
    if (!r) {
        return; /* Nothing to clear */
    }

    ecs_table_t *table = r->table;
    if (table) {
        ecs_table_diff_t diff = {
            .removed = table->type,
            .un_set = { table->storage_ids, table->storage_count }
        };

        delete_entity(world, r, &diff);
        r->table = NULL;
    }    

    flecs_defer_end(world, stage);
error:
    return;
}

static
void flecs_throw_invalid_delete(
    ecs_world_t *world,
    ecs_id_t id)
{
    char *id_str = NULL;
    if (!(world->flags & EcsWorldQuit)) {
        id_str = ecs_id_str(world, id);
        ecs_throw(ECS_CONSTRAINT_VIOLATED, id_str);
    }
error:
    ecs_os_free(id_str);
}

static
void flecs_marked_id_push(
    ecs_world_t *world,
    ecs_id_record_t* idr,
    ecs_entity_t action)
{
    ecs_marked_id_t *m = ecs_vector_add(&world->store.marked_ids, 
        ecs_marked_id_t);

    m->idr = idr;
    m->id = idr->id;
    m->action = action;

    flecs_id_record_claim(world, idr);
}

static
void flecs_id_mark_for_delete(
    ecs_world_t *world,
    ecs_id_record_t *idr,
    ecs_entity_t action);

static
void flecs_targets_mark_for_delete(
    ecs_world_t *world,
    ecs_table_t *table)
{
    ecs_id_record_t *idr;
    ecs_entity_t *entities = ecs_storage_first(&table->data.entities);
    ecs_record_t **records = ecs_storage_first(&table->data.records);
    int32_t i, count = ecs_storage_count(&table->data.entities);
    for (i = 0; i < count; i ++) {
        ecs_record_t *r = records[i];
        if (!r) {
            continue;
        }

        /* If entity is not used as id or as relationship target, there won't
         * be any tables with a reference to it. */
        ecs_flags32_t flags = r->row & ECS_ROW_FLAGS_MASK;
        if (!(flags & (EcsEntityObservedId|EcsEntityObservedObject))) {
            continue;
        }

        ecs_entity_t e = entities[i];
        if (flags & EcsEntityObservedId) {
            if ((idr = flecs_id_record_get(world, e))) {
                flecs_id_mark_for_delete(world, idr, 
                    ECS_ID_ON_DELETE(idr->flags));
            }
            if ((idr = flecs_id_record_get(world, ecs_pair(e, EcsWildcard)))) {
                flecs_id_mark_for_delete(world, idr, 
                    ECS_ID_ON_DELETE(idr->flags));
            }
        }
        if (flags & EcsEntityObservedObject) {
            if ((idr = flecs_id_record_get(world, ecs_pair(EcsWildcard, e)))) {
                flecs_id_mark_for_delete(world, idr, 
                    ECS_ID_ON_DELETE_OBJECT(idr->flags));
            }
        }
    }
}

static
bool flecs_id_is_delete_target(
    ecs_id_t id,
    ecs_entity_t action)
{
    if (!action && ecs_id_is_pair(id) && ECS_PAIR_FIRST(id) == EcsWildcard) {
        /* If no explicit delete action is provided, and the id we're deleting
         * has the form (*, Target), use OnDeleteTarget action */
        return true;
    }
    return false;
}

static
ecs_entity_t flecs_get_delete_action(
    ecs_table_t *table,
    ecs_table_record_t *tr,
    ecs_entity_t action,
    bool delete_target)
{
    ecs_entity_t result = action;
    if (!result && delete_target) {
        /* If action is not specified and we're deleting a relationship target,
         * derive the action from the current record */
        ecs_table_record_t *trr = &table->records[tr->column];
        ecs_id_record_t *idrr = (ecs_id_record_t*)trr->hdr.cache;
        result = ECS_ID_ON_DELETE_OBJECT(idrr->flags);
    }
    return result;
}

static
void flecs_update_monitors_for_delete(
    ecs_world_t *world,
    ecs_id_t id)
{
    update_component_monitors(world, NULL, &(ecs_type_t){
        .array = (ecs_id_t[]){id},
        .count = 1
    });
}

static
void flecs_id_mark_for_delete(
    ecs_world_t *world,
    ecs_id_record_t *idr,
    ecs_entity_t action)
{
    if (idr->flags & EcsIdMarkedForDelete) {
        return;
    }

    idr->flags |= EcsIdMarkedForDelete;
    flecs_marked_id_push(world, idr, action);

    ecs_id_t id = idr->id;

    bool delete_target = flecs_id_is_delete_target(id, action);

    /* Mark all tables with the id for delete */
    ecs_table_cache_iter_t it;
    if (flecs_table_cache_iter(&idr->cache, &it)) {
        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 & EcsTableMarkedForDelete) {
                continue;
            }

            ecs_entity_t cur_action = flecs_get_delete_action(table, tr, action,
                delete_target);

            /* If this is a Delete action, recursively mark ids & tables */
            if (cur_action == EcsDelete) {
                table->flags |= EcsTableMarkedForDelete;
                ecs_log_push_2();
                flecs_targets_mark_for_delete(world, table);
                ecs_log_pop_2();
            } else if (cur_action == EcsPanic) {
                flecs_throw_invalid_delete(world, id);
            }
        }
    }

    /* Same for empty tables */
    if (flecs_table_cache_empty_iter(&idr->cache, &it)) {
        ecs_table_record_t *tr;
        while ((tr = flecs_table_cache_next(&it, ecs_table_record_t))) {
            tr->hdr.table->flags |= EcsTableMarkedForDelete;
        }
    }

    /* Signal query cache monitors */
    flecs_update_monitors_for_delete(world, id);

    /* If id is a wildcard pair, update cache monitors for non-wildcard ids */
    if (ecs_id_is_wildcard(id)) {
        ecs_assert(ECS_HAS_ROLE(id, PAIR), ECS_INTERNAL_ERROR, NULL);
        ecs_id_record_t *cur = idr;
        if (ECS_PAIR_SECOND(id) == EcsWildcard) {
            while ((cur = cur->first.next)) {
                flecs_update_monitors_for_delete(world, cur->id);
            }
        } else {
            ecs_assert(ECS_PAIR_FIRST(id) == EcsWildcard, 
                ECS_INTERNAL_ERROR, NULL);
            while ((cur = cur->second.next)) {
                flecs_update_monitors_for_delete(world, cur->id);
            }
        }
    }
}

static
bool flecs_on_delete_mark(
    ecs_world_t *world,
    ecs_id_t id,
    ecs_entity_t action)
{
    ecs_id_record_t *idr = flecs_id_record_get(world, id);
    if (!idr) {
        /* If there's no id record, there's nothing to delete */
        return false;
    }

    if (!action) {
        /* If no explicit action is provided, derive it */
        if (!ecs_id_is_pair(id) || ECS_PAIR_SECOND(id) == EcsWildcard) {
            /* Delete actions are determined by the component, or in the case
             * of a pair by the relationship. */
            action = ECS_ID_ON_DELETE(idr->flags);
        }
    }

    if (action == EcsPanic) {
        /* This id is protected from deletion */
        flecs_throw_invalid_delete(world, id);
        return false;
    }

    flecs_id_mark_for_delete(world, idr, action);

    return true;
}

static
void flecs_remove_from_table(
    ecs_world_t *world, 
    ecs_table_t *table) 
{
    ecs_table_diff_t temp_diff;
    ecs_diff_buffer_t diff = ECS_DIFF_INIT;
    ecs_table_t *dst_table = table; 

    /* To find the dst table, remove all ids that are marked for deletion */
    int32_t i, t, count = ecs_vector_count(world->store.marked_ids);
    ecs_marked_id_t *ids = ecs_vector_first(world->store.marked_ids,
        ecs_marked_id_t);
    const ecs_table_record_t *tr;
    for (i = 0; i < count; i ++) {
        const ecs_id_record_t *idr = ids[i].idr;

        if (!(tr = flecs_id_record_get_table(idr, dst_table))) {
            continue;
        }

        t = tr->column;

        do {
            ecs_id_t id = dst_table->type.array[t];
            dst_table = flecs_table_traverse_remove(
                world, dst_table, &id, &temp_diff);
            diff_append(&diff, &temp_diff);
        } while (dst_table->type.count && (t = ecs_search_offset(
            world, dst_table, t, idr->id, NULL)) != -1);
    }

    ecs_assert(dst_table != NULL, ECS_INTERNAL_ERROR, NULL);

    if (!dst_table->type.count) {
        /* If this removes all components, clear table */
        ecs_dbg_3("#[red]clear#[reset] entities from table %u", 
            (uint32_t)table->id);
        flecs_table_clear_entities(world, table);
    } else {
        /* Otherwise, merge table into dst_table */
        ecs_dbg_3("#[red]move#[reset] entities from table %u to %u", 
            (uint32_t)table->id, (uint32_t)dst_table->id);

        if (dst_table != table) {
            if (diff.removed.type.count) {
                ecs_log_push_3();
                ecs_table_diff_t td = diff_to_table_diff(&diff);
                flecs_notify_on_remove(world, table, NULL, 
                    0, ecs_table_count(table), &td);
                ecs_log_pop_3();
            }
            flecs_table_merge(world, dst_table, table, 
                &dst_table->data, &table->data);
        }
    }

    diff_free(&diff);
}

static
bool flecs_on_delete_clear_tables(
    ecs_world_t *world)
{
    int32_t i, last = ecs_vector_count(world->store.marked_ids), first = 0;
    ecs_marked_id_t *ids = ecs_vector_first(world->store.marked_ids,
        ecs_marked_id_t);

    /* Iterate in reverse order so that DAGs get deleted bottom to top */
    do {
        for (i = last - 1; i >= first; i --) {
            ecs_id_record_t *idr = ids[i].idr;
            ecs_entity_t action = ids[i].action;
 
            /* Empty all tables for id */
            ecs_table_cache_iter_t it;
            if (flecs_table_cache_iter(&idr->cache, &it)) {
                ecs_table_record_t *tr;
                while ((tr = flecs_table_cache_next(&it, ecs_table_record_t))) {
                    ecs_table_t *table = tr->hdr.table;

                    if ((action == EcsRemove) || 
                        !(table->flags & EcsTableMarkedForDelete))
                    {
                        flecs_remove_from_table(world, table);
                    } else {
                        ecs_dbg_3(
                            "#[red]delete#[reset] entities from table %u", 
                            (uint32_t)table->id);
                        flecs_table_delete_entities(world, table);
                    }
                }
            }

            /* Run commands so children get notified before parent is deleted */
            ecs_defer_end(world);
            ecs_defer_begin(world);

            /* User code (from triggers) could have enqueued more ids to delete,
             * reobtain the array in case it got reallocated */
            ids = ecs_vector_first(world->store.marked_ids, ecs_marked_id_t);
        }

        /* Check if new ids were marked since we started */
        int32_t new_last = ecs_vector_count(world->store.marked_ids);
        if (new_last != last) {
            /* Iterate remaining ids */
            ecs_assert(new_last > last, ECS_INTERNAL_ERROR, NULL);
            first = last;
            last = new_last;
        } else {
            break;
        }
    } while (true);

    return true;
}

static
bool flecs_on_delete_clear_ids(
    ecs_world_t *world)
{
    int32_t i, count = ecs_vector_count(world->store.marked_ids);
    ecs_marked_id_t *ids = ecs_vector_first(world->store.marked_ids,
        ecs_marked_id_t);

    for (i = 0; i < count; i ++) {
        ecs_id_record_t *idr = ids[i].idr;

        flecs_id_record_release_tables(world, idr);

        /* Release the claim taken by flecs_marked_id_push. This may delete the
         * id record as all other claims may have been released. */
        if (flecs_id_record_release(world, idr)) {
            /* If the id record is still alive, release the initial claim */
            flecs_id_record_release(world, idr);
        }
    }

    return true;
}

static
void flecs_on_delete(
    ecs_world_t *world,
    ecs_id_t id,
    ecs_entity_t action)
{
    /* Cleanup can happen recursively. If a cleanup action is already in 
     * progress, only append ids to the marked_ids. The topmost cleanup
     * frame will handle the actual cleanup. */
    int32_t count = ecs_vector_count(world->store.marked_ids);

    /* Make sure we're evaluating a consistent list of non-empty tables */
    ecs_run_aperiodic(world, EcsAperiodicEmptyTables);

    /* Collect all ids that need to be deleted */
    flecs_on_delete_mark(world, id, action);

    /* Only perform cleanup if we're the first stack frame doing it */
    if (!count && ecs_vector_count(world->store.marked_ids)) {
        ecs_dbg_2("#[red]delete#[reset]");
        ecs_log_push_2();

        /* Empty tables with all the to be deleted ids */
        flecs_on_delete_clear_tables(world);

        /* All marked tables are empty, ensure they're in the right list */
        ecs_run_aperiodic(world, EcsAperiodicEmptyTables);

        /* Release remaining references to the ids */
        flecs_on_delete_clear_ids(world);

        /* Verify deleted ids are no longer in use */
#ifdef FLECS_DEBUG
        ecs_marked_id_t *ids = ecs_vector_first(world->store.marked_ids,
            ecs_marked_id_t);
        int32_t i;
        count = ecs_vector_count(world->store.marked_ids);
        for (i = 0; i < count; i ++) {
            ecs_assert(!ecs_id_in_use(world, ids[i].id), 
                ECS_INTERNAL_ERROR, NULL);
        }
#endif
        ecs_assert(!ecs_id_in_use(world, id), ECS_INTERNAL_ERROR, NULL);

        /* Ids are deleted, clear stack */
        ecs_vector_clear(world->store.marked_ids);

        ecs_log_pop_2();
    }
}

void ecs_delete_with(
    ecs_world_t *world,
    ecs_id_t id)
{
    ecs_stage_t *stage = flecs_stage_from_world(&world);
    if (flecs_defer_on_delete_action(world, stage, id, EcsDelete)) {
        return;
    }

    flecs_on_delete(world, id, EcsDelete);
    flecs_defer_end(world, stage);
}

void ecs_remove_all(
    ecs_world_t *world,
    ecs_id_t id)
{
    ecs_stage_t *stage = flecs_stage_from_world(&world);
    if (flecs_defer_on_delete_action(world, stage, id, EcsRemove)) {
        return;
    }

    flecs_on_delete(world, id, EcsRemove);
    flecs_defer_end(world, stage);
}

void ecs_delete(
    ecs_world_t *world,
    ecs_entity_t entity)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(entity != 0, ECS_INVALID_PARAMETER, NULL);

    ecs_stage_t *stage = flecs_stage_from_world(&world);
    if (flecs_defer_delete(world, stage, entity)) {
        return;
    }

    ecs_record_t *r = flecs_entities_get(world, entity);
    if (r) {
        ecs_flags32_t row_flags = ECS_RECORD_TO_ROW_FLAGS(r->row);
        if (row_flags) {
            if (row_flags & EcsEntityObservedId) {
                flecs_on_delete(world, entity, 0);
                flecs_on_delete(world, ecs_pair(entity, EcsWildcard), 0);
            }
            if (row_flags & EcsEntityObservedObject) {
                flecs_on_delete(world, ecs_pair(EcsWildcard, entity), 0);
            }

            /* Merge operations before deleting entity */
            ecs_defer_end(world);
            ecs_defer_begin(world);
        }

        ecs_table_t *table = r->table;

        /* If entity has components, remove them. Check if table is still alive,
         * as delete actions could have deleted the table already. */
        if (table) {
            ecs_table_diff_t diff = {
                .removed = table->type,
                .un_set = { table->storage_ids, table->storage_count }
            };

            delete_entity(world, r, &diff);

            r->row = 0;
            r->table = NULL;
        }
        
        flecs_entities_remove(world, entity);
    }

    flecs_defer_end(world, stage);
error:
    return;
}

void ecs_add_id(
    ecs_world_t *world,
    ecs_entity_t entity,
    ecs_id_t id)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(ecs_is_valid(world, entity), ECS_INVALID_PARAMETER, NULL);
    ecs_check(ecs_id_is_valid(world, id), ECS_INVALID_PARAMETER, NULL);
    add_id(world, entity, id);
error:
    return;
}

void ecs_remove_id(
    ecs_world_t *world,
    ecs_entity_t entity,
    ecs_id_t id)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(ecs_is_valid(world, entity), ECS_INVALID_PARAMETER, NULL);
    ecs_check(ecs_id_is_valid(world, id) || ecs_id_is_wildcard(id), 
        ECS_INVALID_PARAMETER, NULL);
    remove_id(world, entity, id);
error:
    return;
}

ecs_entity_t ecs_clone(
    ecs_world_t *world,
    ecs_entity_t dst,
    ecs_entity_t src,
    bool copy_value)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(src != 0, ECS_INVALID_PARAMETER, NULL);
    ecs_check(ecs_is_valid(world, src), ECS_INVALID_PARAMETER, NULL);
    ecs_check(!dst || !ecs_get_table(world, dst), ECS_INVALID_PARAMETER, NULL);

    ecs_stage_t *stage = flecs_stage_from_world(&world);
    if (!dst) {
        dst = ecs_new_id(world);
    }

    if (flecs_defer_clone(world, stage, dst, src, copy_value)) {
        return dst;
    }

    ecs_record_t *src_r = flecs_entities_get(world, src);
    ecs_table_t *src_table;
    if (!src_r || !(src_table = src_r->table)) {
        goto done;
    }

    ecs_type_t src_type = src_table->type;
    ecs_table_diff_t diff = { .added = src_type };
    ecs_record_t *dst_r = new_entity(world, dst, NULL, src_table, &diff, true, true);
    int32_t row = ECS_RECORD_TO_ROW(dst_r->row);

    if (copy_value) {
        flecs_table_move(world, dst, src, src_table,
            row, src_table, ECS_RECORD_TO_ROW(src_r->row), true);

        flecs_notify_on_set(world, src_table, row, 1, NULL, true);
    }

done:
    flecs_defer_end(world, stage);
    return dst;
error:
    return 0;
}

const void* ecs_get_id(
    const ecs_world_t *world,
    ecs_entity_t entity,
    ecs_id_t id)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(ecs_is_valid(world, entity), ECS_INVALID_PARAMETER, NULL);
    ecs_check(flecs_stage_from_readonly_world(world)->asynchronous == false, 
        ECS_INVALID_PARAMETER, NULL);

    world = ecs_get_world(world);

    ecs_record_t *r = flecs_entities_get(world, entity);
    if (!r) {
        return NULL;
    }

    ecs_table_t *table = r->table;
    if (!table) {
        return NULL;
    }

    ecs_id_record_t *idr = flecs_id_record_get(world, id);
    if (!idr) {
        return NULL;
    }

    const ecs_table_record_t *tr = NULL;
    ecs_table_t *storage_table = table->storage_table;
    if (storage_table) {
        tr = flecs_id_record_get_table(idr, storage_table);
    } else {
        /* If the entity does not have a storage table (has no data) but it does
         * have the id, the id must be a tag, and getting a tag is illegal. */
        ecs_check(!ecs_owns_id(world, entity, id), ECS_NOT_A_COMPONENT, NULL);
    }

    if (!tr) {
       return get_base_component(world, table, id, idr, 0);
    }

    int32_t row = ECS_RECORD_TO_ROW(r->row);
    return get_component_w_index(table, tr->column, row);
error:
    return NULL;
}

void* ecs_get_mut_id(
    ecs_world_t *world,
    ecs_entity_t entity,
    ecs_id_t id)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(ecs_is_valid(world, entity), ECS_INVALID_PARAMETER, NULL);
    ecs_check(ecs_id_is_valid(world, id), ECS_INVALID_PARAMETER, NULL);
    
    ecs_stage_t *stage = flecs_stage_from_world(&world);
    void *result;

    if (flecs_defer_set(
        world, stage, EcsOpMut, entity, id, 0, NULL, &result))
    {
        return result;
    }

    ecs_record_t *r = flecs_entities_ensure(world, entity);
    result = get_mutable(world, entity, id, r);
    ecs_check(result != NULL, ECS_INVALID_PARAMETER, NULL);
    
    flecs_defer_end(world, stage);

    return result;
error:
    return NULL;
}

static
ecs_record_t* flecs_access_begin(
    ecs_world_t *stage,
    ecs_entity_t entity,
    bool write)
{
    ecs_check(ecs_os_has_threading(), ECS_MISSING_OS_API, NULL);

    const ecs_world_t *world = ecs_get_world(stage);
    ecs_record_t *r = flecs_entities_get(world, entity);
    if (!r) {
        return NULL;
    }

    ecs_table_t *table;
    if (!(table = r->table)) {
        return NULL;
    }

    int32_t count = ecs_os_ainc(&table->lock);
    (void)count;
    if (write) {
        ecs_check(count == 1, ECS_ACCESS_VIOLATION, NULL);
    }

    return r;
error:
    return NULL;
}

static
void flecs_access_end(
    const ecs_record_t *r,
    bool write)
{
    ecs_check(ecs_os_has_threading(), ECS_MISSING_OS_API, NULL);
    ecs_check(r != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(r->table != NULL, ECS_INVALID_PARAMETER, NULL);
    int32_t count = ecs_os_adec(&r->table->lock);
    (void)count;
    if (write) {
        ecs_check(count == 0, ECS_ACCESS_VIOLATION, NULL);
    }
    ecs_check(count >= 0, ECS_ACCESS_VIOLATION, NULL);

error:
    return;
}

ecs_record_t* ecs_write_begin(
    ecs_world_t *world,
    ecs_entity_t entity)
{
    return flecs_access_begin(world, entity, true);
}

void ecs_write_end(
    ecs_record_t *r)
{
    flecs_access_end(r, true);
}

const ecs_record_t* ecs_read_begin(
    ecs_world_t *world,
    ecs_entity_t entity)
{
    return flecs_access_begin(world, entity, false);
}

void ecs_read_end(
    const ecs_record_t *r)
{
    flecs_access_end(r, false);
}

const void* ecs_record_get_id(
    ecs_world_t *stage,
    const ecs_record_t *r,
    ecs_id_t id)
{
    const ecs_world_t *world = ecs_get_world(stage);
    return get_component(world, r->table, ECS_RECORD_TO_ROW(r->row), id);
}

void* ecs_record_get_mut_id(
    ecs_world_t *stage,
    ecs_record_t *r,
    ecs_id_t id)
{
    const ecs_world_t *world = ecs_get_world(stage);
    return get_component(world, r->table, ECS_RECORD_TO_ROW(r->row), id);
}

ecs_ref_t ecs_ref_init_id(
    const ecs_world_t *world,
    ecs_entity_t entity,
    ecs_id_t id)
{
    ecs_check(ecs_is_valid(world, entity), ECS_INVALID_PARAMETER, NULL);
    ecs_check(ecs_id_is_valid(world, id), ECS_INVALID_PARAMETER, NULL);
    
    world = ecs_get_world(world);

    ecs_record_t *record = flecs_entities_get(world, entity);
    ecs_check(record != NULL, ECS_INVALID_PARAMETER,
        "cannot create ref for empty entity");

    ecs_ref_t result = {
        .entity = entity,
        .id = id,
        .record = record
    };

    ecs_table_t *table = record->table;
    if (table) {
        result.tr = flecs_table_record_get(world, table->storage_table, id);
    }

    return result;
error:
    return (ecs_ref_t){0};
}

void* ecs_ref_get_id(
    const ecs_world_t *world,
    ecs_ref_t *ref,
    ecs_id_t id)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(ref != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(ref->entity != 0, ECS_INVALID_PARAMETER, NULL);
    ecs_check(ref->id != 0, ECS_INVALID_PARAMETER, NULL);
    ecs_check(ref->record != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(id == ref->id, ECS_INVALID_PARAMETER, NULL);

    ecs_record_t *r = ref->record;
    ecs_table_t *table = r->table;
    if (!table) {
        return NULL;
    }

    int32_t row = ECS_RECORD_TO_ROW(r->row);
    ecs_check(row < ecs_table_count(table), ECS_INTERNAL_ERROR, NULL);

    ecs_table_record_t *tr = ref->tr;
    if (!tr || tr->hdr.table != table) {
        tr = ref->tr = flecs_table_record_get(world, table->storage_table, id);
        if (!tr) {
            return NULL;
        }
    }

    return get_component_w_index(table, tr->column, row);
error:
    return NULL;
}

void* ecs_emplace_id(
    ecs_world_t *world,
    ecs_entity_t entity,
    ecs_id_t id)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(ecs_is_valid(world, entity), ECS_INVALID_PARAMETER, NULL);
    ecs_check(ecs_id_is_valid(world, id), ECS_INVALID_PARAMETER, NULL);
    ecs_check(!ecs_has_id(world, entity, id), ECS_INVALID_PARAMETER, NULL);

    ecs_stage_t *stage = flecs_stage_from_world(&world);
    void *result;

    if (flecs_defer_set(world, stage, EcsOpMut, entity, id, 0, NULL, &result)) {
        return result;
    }

    ecs_record_t *r = flecs_entities_ensure(world, entity);
    add_id_w_record(world, entity, r, id, false /* Add without ctor */);

    void *ptr = get_component(world, r->table, ECS_RECORD_TO_ROW(r->row), id);
    ecs_check(ptr != NULL, ECS_INVALID_PARAMETER, NULL);

    flecs_defer_end(world, stage);

    return ptr;
error:
    return NULL;
}

void ecs_modified_id(
    ecs_world_t *world,
    ecs_entity_t entity,
    ecs_id_t id)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(ecs_is_valid(world, entity), ECS_INVALID_PARAMETER, NULL);
    ecs_check(ecs_id_is_valid(world, id), ECS_INVALID_PARAMETER, NULL);

    ecs_stage_t *stage = flecs_stage_from_world(&world);

    if (flecs_defer_modified(world, stage, entity, id)) {
        return;
    }

    /* If the entity does not have the component, calling ecs_modified is 
     * invalid. The assert needs to happen after the defer statement, as the
     * entity may not have the component when this function is called while
     * operations are being deferred. */
    ecs_check(ecs_has_id(world, entity, id), ECS_INVALID_PARAMETER, NULL);

    ecs_record_t *r = flecs_entities_get(world, entity);
    ecs_table_t *table = r->table;
    ecs_type_t ids = { .array = &id, .count = 1 };
    flecs_notify_on_set(world, table, ECS_RECORD_TO_ROW(r->row), 1, &ids, true);

    flecs_table_mark_dirty(world, table, id);
    flecs_defer_end(world, stage);
error:
    return;
}

static
ecs_entity_t set_ptr_w_id(
    ecs_world_t *world,
    ecs_entity_t entity,
    ecs_id_t id,
    size_t size,
    void *ptr,
    bool is_move,
    bool flecs_notify)
{
    ecs_stage_t *stage = flecs_stage_from_world(&world);

    if (!entity) {
        entity = ecs_new_id(world);
        ecs_entity_t scope = stage->scope;
        if (scope) {
            ecs_add_pair(world, entity, EcsChildOf, scope);
        }
    }

    if (flecs_defer_set(world, stage, EcsOpSet, entity, id, 
        flecs_utosize(size), ptr, NULL))
    {
        return entity;
    }

    ecs_record_t *r = flecs_entities_ensure(world, entity);
    void *dst = get_mutable(world, entity, id, r);
    ecs_check(dst != NULL, ECS_INVALID_PARAMETER, NULL);

    if (ptr) {
        ecs_entity_t real_id = ecs_get_typeid(world, id);
        const ecs_type_info_t *ti = get_c_info(world, real_id);
        if (ti) {
            if (is_move) {
                ecs_move_t move = ti->hooks.move;
                if (move) {
                    move(dst, ptr, 1, ti);
                } else {
                    ecs_os_memcpy(dst, ptr, flecs_utosize(size));
                }
            } else {
                ecs_copy_t copy = ti->hooks.copy;
                if (copy) {
                    copy(dst, ptr, 1, ti);
                } else {
                    ecs_os_memcpy(dst, ptr, flecs_utosize(size));
                }
            }
        } else {
            ecs_os_memcpy(dst, ptr, flecs_utosize(size));
        }
    } else {
        ecs_os_memset(dst, 0, size);
    }

    flecs_table_mark_dirty(world, r->table, id);

    if (flecs_notify) {
        ecs_type_t ids = { .array = &id, .count = 1 };
        flecs_notify_on_set(
            world, r->table, ECS_RECORD_TO_ROW(r->row), 1, &ids, true);
    }

    flecs_defer_end(world, stage);

    return entity;
error:
    return 0;
}

ecs_entity_t ecs_set_id(
    ecs_world_t *world,
    ecs_entity_t entity,
    ecs_id_t id,
    size_t size,
    const void *ptr)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(!entity || ecs_is_valid(world, entity), ECS_INVALID_PARAMETER, NULL);
    ecs_check(ecs_id_is_valid(world, id), ECS_INVALID_PARAMETER, NULL);

    /* Safe to cast away const: function won't modify if move arg is false */
    return set_ptr_w_id(
        world, entity, id, size, (void*)ptr, false, true);
error:
    return 0;
}

void ecs_enable_component_w_id(
    ecs_world_t *world,
    ecs_entity_t entity,
    ecs_id_t id,
    bool enable)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(ecs_is_valid(world, entity), ECS_INVALID_PARAMETER, NULL);
    ecs_check(ecs_id_is_valid(world, id), ECS_INVALID_PARAMETER, NULL);

    ecs_stage_t *stage = flecs_stage_from_world(&world);

    if (flecs_defer_enable(
        world, stage, entity, id, enable))
    {
        return;
    } else {
        /* Operations invoked by enable/disable should not be deferred */
        stage->defer --;
    }

    ecs_record_t *r = flecs_entities_ensure(world, entity);
    ecs_entity_t bs_id = (id & ECS_COMPONENT_MASK) | ECS_DISABLED;
    
    ecs_table_t *table = r->table;
    int32_t index = -1;
    if (table) {
        index = ecs_search(world, table, bs_id, 0);
    }

    if (index == -1) {
        ecs_add_id(world, entity, bs_id);
        ecs_enable_component_w_id(world, entity, id, enable);
        return;
    }

    index -= table->bs_offset;
    ecs_assert(index >= 0, ECS_INTERNAL_ERROR, NULL);

    /* Data cannot be NULl, since entity is stored in the table */
    ecs_bitset_t *bs = &table->data.bs_columns[index];
    ecs_assert(bs != NULL, ECS_INTERNAL_ERROR, NULL);

    flecs_bitset_set(bs, ECS_RECORD_TO_ROW(r->row), enable);
error:
    return;
}

bool ecs_is_component_enabled_w_id(
    const ecs_world_t *world,
    ecs_entity_t entity,
    ecs_id_t id)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(ecs_is_valid(world, entity), ECS_INVALID_PARAMETER, NULL);
    ecs_check(ecs_id_is_valid(world, id), ECS_INVALID_PARAMETER, NULL);

    /* Make sure we're not working with a stage */
    world = ecs_get_world(world);

    ecs_record_t *r = flecs_entities_get(world, entity);
    ecs_table_t *table;
    if (!r || !(table = r->table)) {
        return false;
    }

    ecs_entity_t bs_id = (id & ECS_COMPONENT_MASK) | ECS_DISABLED;
    int32_t index = ecs_search(world, table, bs_id, 0);
    if (index == -1) {
        /* If table does not have DISABLED column for component, component is
         * always enabled, if the entity has it */
        return ecs_has_id(world, entity, id);
    }

    index -= table->bs_offset;
    ecs_assert(index >= 0, ECS_INTERNAL_ERROR, NULL);
    ecs_bitset_t *bs = &table->data.bs_columns[index];

    return flecs_bitset_get(bs, ECS_RECORD_TO_ROW(r->row));
error:
    return false;
}

bool ecs_has_id(
    const ecs_world_t *world,
    ecs_entity_t entity,
    ecs_id_t id)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(ecs_is_valid(world, entity), ECS_INVALID_PARAMETER, NULL);
    ecs_check(id != 0, ECS_INVALID_PARAMETER, NULL);

    /* Make sure we're not working with a stage */
    world = ecs_get_world(world);

    ecs_record_t *r = flecs_entities_get(world, entity);
    ecs_table_t *table;
    if (!r || !(table = r->table)) {
        return false;
    }

    ecs_table_record_t *tr = NULL;
    int32_t column = ecs_search_relation(
        world, table, 0, id, EcsIsA, 0, 0, 0, &tr);
    if (column == -1) {
        return false;
    }

    table = tr->hdr.table;
    if ((table->flags & EcsTableHasUnion) && ECS_HAS_ROLE(id, PAIR) &&
        ECS_PAIR_SECOND(id) != EcsWildcard) 
    {
        if (ECS_PAIR_FIRST(table->type.array[column]) == EcsUnion) {
            ecs_switch_t *sw = &table->data.sw_columns[
                column - table->sw_offset];
            int32_t row = ECS_RECORD_TO_ROW(r->row);
            uint64_t value = flecs_switch_get(sw, row);
            return value == ECS_PAIR_SECOND(id);
        }
    }
    
    return true;
error:
    return false;
}

ecs_entity_t ecs_get_target(
    const ecs_world_t *world,
    ecs_entity_t entity,
    ecs_entity_t rel,
    int32_t index)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(entity != 0, ECS_INVALID_PARAMETER, NULL);
    ecs_check(rel != 0, ECS_INVALID_PARAMETER, NULL);

    world = ecs_get_world(world);

    ecs_record_t *r = flecs_entities_get(world, entity);
    ecs_table_t *table;
    if (!r || !(table = r->table)) {
        return 0;
    }

    ecs_id_t wc = ecs_pair(rel, EcsWildcard);
    ecs_table_record_t *tr = flecs_table_record_get(world, table, wc);
    if (!tr) {
        if (table->flags & EcsTableHasUnion) {
            wc = ecs_pair(EcsUnion, rel);
            tr = flecs_table_record_get(world, table, wc);
            if (tr) {
                ecs_switch_t *sw = &table->data.sw_columns[
                    tr->column - table->sw_offset];
                int32_t row = ECS_RECORD_TO_ROW(r->row);
                return flecs_switch_get(sw, row);
                
            }
        }
        return 0;
    }

    if (index >= tr->count) {
        return 0;
    }

    ecs_id_t *ids = table->type.array;
    return ecs_pair_second(world, ids[tr->column + index]);
error:
    return 0;
}

ecs_entity_t ecs_get_target_for_id(
    const ecs_world_t *world,
    ecs_entity_t entity,
    ecs_entity_t rel,
    ecs_id_t id)
{
    ecs_table_t *table = ecs_get_table(world, entity);
    ecs_entity_t subject = 0;

    if (rel) {
        int32_t column = ecs_search_relation(
            world, table, 0, id, rel, 0, &subject, 0, 0);
        if (column == -1) {
            return 0;
        }
    } else {
        ecs_id_t *ids = table->type.array;
        int32_t i, count = table->type.count;

        for (i = 0; i < count; i ++) {
            ecs_id_t ent = ids[i];
            if (ent & ECS_ROLE_MASK) {
                /* Skip ids with pairs, roles since 0 was provided for rel */
                break;
            }

            if (ecs_has_id(world, ent, id)) {
                subject = ent;
                break;
            }
        }
    }

    if (subject == 0) {
        return entity;
    } else {
        return subject;
    }
}

static
const char* get_identifier(
    const ecs_world_t *world,
    ecs_entity_t entity,
    ecs_entity_t tag)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(ecs_is_valid(world, entity), ECS_INVALID_PARAMETER, NULL);

    const EcsIdentifier *ptr = ecs_get_pair(
        world, entity, EcsIdentifier, tag);

    if (ptr) {
        return ptr->value;
    } else {
        return NULL;
    }
error:
    return NULL;
}

const char* ecs_get_name(
    const ecs_world_t *world,
    ecs_entity_t entity)
{
    return get_identifier(world, entity, EcsName);
}

const char* ecs_get_symbol(
    const ecs_world_t *world,
    ecs_entity_t entity)
{
    return get_identifier(world, entity, EcsSymbol);
}

static
ecs_entity_t set_identifier(
    ecs_world_t *world,
    ecs_entity_t entity,
    ecs_entity_t tag,
    const char *name)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    if (!entity) {
        entity = ecs_new_id(world);
    }

    EcsIdentifier *ptr = ecs_get_mut_pair(world, entity, EcsIdentifier, tag);
    ecs_assert(ptr != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_os_strset(&ptr->value, name);
    ecs_modified_pair(world, entity, ecs_id(EcsIdentifier), tag);
    
    return entity;
error:
    return 0;
}


ecs_entity_t ecs_set_name(
    ecs_world_t *world,
    ecs_entity_t entity,
    const char *name)
{
    if (!entity) {
        return ecs_entity_init(world, &(ecs_entity_desc_t){
            .name = name
        });
    }
    return set_identifier(world, entity, EcsName, name);
}

ecs_entity_t ecs_set_symbol(
    ecs_world_t *world,
    ecs_entity_t entity,
    const char *name)
{
    return set_identifier(world, entity, EcsSymbol, name);
}

void ecs_set_alias(
    ecs_world_t *world,
    ecs_entity_t entity,
    const char *name)
{
    set_identifier(world, entity, EcsAlias, name);
}

ecs_id_t ecs_make_pair(
    ecs_entity_t relationship,
    ecs_entity_t target)
{
    return ecs_pair(relationship, target);
}

bool ecs_is_valid(
    const ecs_world_t *world,
    ecs_entity_t entity)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);

    /* 0 is not a valid entity id */
    if (!entity) {
        return false;
    }
    
    /* Make sure we're not working with a stage */
    world = ecs_get_world(world);
    
    /* Entity identifiers should not contain flag bits */
    if (entity & ECS_ROLE_MASK) {
        return false;
    }

    /* Entities should not contain data in dead zone bits */
    if (entity & ~0xFF00FFFFFFFFFFFF) {
        return false;
    }

    if (entity & ECS_ROLE) {
        return ecs_entity_t_lo(entity) != 0;
    }

    /* If entity doesn't exist in the world, the id is valid as long as the
     * generation is 0. Using a non-existing id with a non-zero generation
     * requires calling ecs_ensure first. */
    if (!ecs_exists(world, entity)) {
        return ECS_GENERATION(entity) == 0;
    }

    /* If id exists, it must be alive (the generation count must match) */
    return ecs_is_alive(world, entity);
error:
    return false;
}

ecs_id_t ecs_strip_generation(
    ecs_entity_t e)
{
    /* If this is not a pair, erase the generation bits */
    if (!(e & ECS_ROLE_MASK)) {
        e &= ~ECS_GENERATION_MASK;
    }

    return e;
}

bool ecs_is_alive(
    const ecs_world_t *world,
    ecs_entity_t entity)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(entity != 0, ECS_INVALID_PARAMETER, NULL);

    world = ecs_get_world(world);

    return flecs_entities_is_alive(world, entity);
error:
    return false;
}

ecs_entity_t ecs_get_alive(
    const ecs_world_t *world,
    ecs_entity_t entity)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    
    if (!entity) {
        return 0;
    }

    if (ecs_is_alive(world, entity)) {
        return entity;
    }

    /* Make sure id does not have generation. This guards against accidentally
     * "upcasting" a not alive identifier to a alive one. */
    ecs_assert((uint32_t)entity == entity, ECS_INVALID_PARAMETER, NULL);

    /* Make sure we're not working with a stage */
    world = ecs_get_world(world);

    ecs_entity_t current = flecs_entities_get_current(world, entity);
    if (!current || !ecs_is_alive(world, current)) {
        return 0;
    }

    return current;
error:
    return 0;
}

void ecs_ensure(
    ecs_world_t *world,
    ecs_entity_t entity)
{
    ecs_poly_assert(world, ecs_world_t); /* Cannot be a stage */
    ecs_check(entity != 0, ECS_INVALID_PARAMETER, NULL);

    /* Check if a version of the provided id is alive */
    ecs_entity_t any = ecs_get_alive(world, ecs_strip_generation(entity));
    if (any == entity) {
        /* If alive and equal to the argument, there's nothing left to do */
        return;
    }

    /* If the id is currently alive but did not match the argument, fail */
    ecs_check(!any, ECS_INVALID_PARAMETER, NULL);

    /* Set generation if not alive. The sparse set checks if the provided
     * id matches its own generation which is necessary for alive ids. This
     * check would cause ecs_ensure to fail if the generation of the 'entity'
     * argument doesn't match with its generation.
     * 
     * While this could've been addressed in the sparse set, this is a rare
     * scenario that can only be triggered by ecs_ensure. Implementing it here
     * allows the sparse set to not do this check, which is more efficient. */
    flecs_entities_set_generation(world, entity);

    /* Ensure id exists. The underlying datastructure will verify that the
     * generation count matches the provided one. */
    flecs_entities_ensure(world, entity);
error:
    return;
}

void ecs_ensure_id(
    ecs_world_t *world,
    ecs_id_t id)
{
    if (ECS_HAS_ROLE(id, PAIR)) {
        ecs_entity_t r = ECS_PAIR_FIRST(id);
        ecs_entity_t o = ECS_PAIR_SECOND(id);

        ecs_check(r != 0, ECS_INVALID_PARAMETER, NULL);
        ecs_check(o != 0, ECS_INVALID_PARAMETER, NULL);

        if (ecs_get_alive(world, r) == 0) {
            ecs_ensure(world, r);
        }
        if (ecs_get_alive(world, o) == 0) {
            ecs_ensure(world, o);
        }
    } else {
        ecs_ensure(world, id & ECS_COMPONENT_MASK);
    }
error:
    return;
}

bool ecs_exists(
    const ecs_world_t *world,
    ecs_entity_t entity)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(entity != 0, ECS_INVALID_PARAMETER, NULL);

    world = ecs_get_world(world);

    return flecs_entities_exists(world, entity);
error:
    return false;
}

ecs_table_t* ecs_get_table(
    const ecs_world_t *world,
    ecs_entity_t entity)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(ecs_is_valid(world, entity), ECS_INVALID_PARAMETER, NULL);
    
    world = ecs_get_world(world);

    ecs_record_t *record = flecs_entities_get(world, entity);
    ecs_table_t *table;
    if (record && (table = record->table)) {
        return table;
    }
error:
    return NULL;
}

ecs_table_t* ecs_get_storage_table(
    const ecs_world_t *world,
    ecs_entity_t entity)
{
    ecs_table_t *table = ecs_get_table(world, entity);
    if (table) {
       return table->storage_table;
    }

    return NULL;   
}

const ecs_type_t* ecs_get_type(
    const ecs_world_t *world,
    ecs_entity_t entity)
{
    ecs_table_t *table = ecs_get_table(world, entity);
    if (table) {
        return &table->type;
    }

    return NULL;
}

const ecs_type_info_t* ecs_get_type_info(
    const ecs_world_t *world,
    ecs_id_t id)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);

    world = ecs_get_world(world);

    ecs_id_record_t *idr = flecs_id_record_get(world, id);
    if (!idr && ECS_HAS_ROLE(id, PAIR)) {
        idr = flecs_id_record_get(world, 
            ecs_pair(ECS_PAIR_FIRST(id), EcsWildcard));
        if (!idr || !idr->type_info) {
            idr = NULL;
        }
        if (!idr) {
            idr = flecs_id_record_get(world, 
                ecs_pair(EcsWildcard, ECS_PAIR_SECOND(id)));
            if (!idr || !idr->type_info) {
                idr = NULL;
            }
        }
    }

    if (idr) {
        return idr->type_info;
    } else if (!(id & ECS_ROLE_MASK)) {
        return flecs_sparse_get(world->type_info, ecs_type_info_t, id);
    }
error:
    return NULL;
}

ecs_entity_t ecs_get_typeid(
    const ecs_world_t *world,
    ecs_id_t id)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    const ecs_type_info_t *ti = ecs_get_type_info(world, id);
    if (ti) {
        return ti->component;
    }
error:
    return 0;
}

ecs_entity_t ecs_id_is_tag(
    const ecs_world_t *world,
    ecs_id_t id)
{
    if (ecs_id_is_wildcard(id)) {
        /* If id is a wildcard, we can't tell if it's a tag or not, except
         * when the relationship part of a pair has the Tag property */
        if (ECS_HAS_ROLE(id, PAIR)) {
            if (ECS_PAIR_FIRST(id) != EcsWildcard) {
                ecs_entity_t rel = ecs_pair_first(world, id);
                if (ecs_is_valid(world, rel)) {
                    if (ecs_has_id(world, rel, EcsTag)) {
                        return true;
                    }
                } else {
                    /* During bootstrap it's possible that not all ids are valid
                    * yet. Using ecs_get_typeid will ensure correct values are
                    * returned for only those components initialized during
                    * bootstrap, while still asserting if another invalid id
                    * is provided. */
                    if (ecs_get_typeid(world, id) == 0) {
                        return true;
                    }
                }
            } else {
                /* If relationship is * id is not guaranteed to be a tag */
            }
        }
    } else {
        if (ecs_get_typeid(world, id) == 0) {
            return true;
        }
    }

    return false;
}

int32_t ecs_count_id(
    const ecs_world_t *world,
    ecs_entity_t id)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);

    if (!id) {
        return 0;
    }

    int32_t count = 0;
    ecs_iter_t it = ecs_term_iter(world, &(ecs_term_t) { .id = id });
    while (ecs_term_next(&it)) {
        count += it.count;
    }

    return count;
error:
    return 0;
}

void ecs_enable(
    ecs_world_t *world,
    ecs_entity_t entity,
    bool enabled)
{
    if (ecs_has_id(world, entity, EcsPrefab)) {
        /* If entity is a type, enable/disable all entities in the type */
        const ecs_type_t *type = ecs_get_type(world, entity);
        ecs_assert(type != NULL, ECS_INTERNAL_ERROR, NULL);
        ecs_id_t *ids = type->array;
        int32_t i, count = type->count;
        for (i = 0; i < count; i ++) {
            ecs_id_t id = ids[i];
            if (ecs_id_get_flags(world, id) & EcsIdDontInherit) {
                continue;
            }
            ecs_enable(world, id, enabled);
        }
    } else {
        if (enabled) {
            ecs_remove_id(world, entity, EcsDisabled);
        } else {
            ecs_add_id(world, entity, EcsDisabled);
        }
    }
}

bool ecs_defer_begin(
    ecs_world_t *world)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_stage_t *stage = flecs_stage_from_world(&world);
    return flecs_defer_begin(world, stage);
error:
    return false;
}

bool ecs_defer_end(
    ecs_world_t *world)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_stage_t *stage = flecs_stage_from_world(&world);
    return flecs_defer_end(world, stage);
error:
    return false;
}

void ecs_defer_suspend(
    ecs_world_t *world)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(ecs_is_deferred(world), ECS_INVALID_OPERATION, NULL);
    ecs_stage_t *stage = flecs_stage_from_world(&world);
    ecs_check(stage->defer_suspend == false, ECS_INVALID_OPERATION, NULL);
    stage->defer_suspend = true;
error:
    return;
}

void ecs_defer_resume(
    ecs_world_t *world)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(ecs_is_deferred(world), ECS_INVALID_OPERATION, NULL);
    ecs_stage_t *stage = flecs_stage_from_world(&world);
    ecs_check(stage->defer_suspend == true, ECS_INVALID_OPERATION, NULL);
    stage->defer_suspend = false;
error:
    return;
}

const char* ecs_role_str(
    ecs_entity_t entity)
{
    if (ECS_HAS_ROLE(entity, PAIR)) {
        return "PAIR";
    } else
    if (ECS_HAS_ROLE(entity, DISABLED)) {
        return "DISABLED";
    } else    
    if (ECS_HAS_ROLE(entity, XOR)) {
        return "XOR";
    } else
    if (ECS_HAS_ROLE(entity, OR)) {
        return "OR";
    } else
    if (ECS_HAS_ROLE(entity, AND)) {
        return "AND";
    } else
    if (ECS_HAS_ROLE(entity, NOT)) {
        return "NOT";
    } else
    if (ECS_HAS_ROLE(entity, OVERRIDE)) {
        return "OVERRIDE";
    } else {
        return "UNKNOWN";
    }
}

void ecs_id_str_buf(
    const ecs_world_t *world,
    ecs_id_t id,
    ecs_strbuf_t *buf)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);

    world = ecs_get_world(world);

    if (id & ECS_ROLE_MASK && !ECS_HAS_ROLE(id, PAIR)) {
        ecs_strbuf_appendstr(buf, ecs_role_str(id));
        ecs_strbuf_appendch(buf, '|');
    }

    if (ECS_HAS_ROLE(id, PAIR)) {
        ecs_entity_t rel = ECS_PAIR_FIRST(id);
        ecs_entity_t obj = ECS_PAIR_SECOND(id);

        ecs_entity_t e;
        if ((e = ecs_get_alive(world, rel))) {
            rel = e;
        }
        if ((e = ecs_get_alive(world, obj))) {
            obj = e;
        }

        ecs_strbuf_appendch(buf, '(');
        ecs_get_path_w_sep_buf(world, 0, rel, NULL, NULL, buf);
        ecs_strbuf_appendch(buf, ',');
        ecs_get_path_w_sep_buf(world, 0, obj, NULL, NULL, buf);
        ecs_strbuf_appendch(buf, ')');
    } else {
        ecs_entity_t e = id & ECS_COMPONENT_MASK;
        ecs_get_path_w_sep_buf(world, 0, e, NULL, NULL, buf);
    }

error:
    return;
}

char* ecs_id_str(
    const ecs_world_t *world,
    ecs_id_t id)
{
    ecs_strbuf_t buf = ECS_STRBUF_INIT;
    ecs_id_str_buf(world, id, &buf);
    return ecs_strbuf_get(&buf);
}

char* ecs_type_str(
    const ecs_world_t *world,
    const ecs_type_t *type)
{
    if (!type) {
        return ecs_os_strdup("");
    }

    ecs_strbuf_t buf = ECS_STRBUF_INIT;
    ecs_entity_t *ids = type->array;
    int32_t i, count = type->count;

    for (i = 0; i < count; i ++) {
        ecs_entity_t id = ids[i];

        if (i) {
            ecs_strbuf_appendch(&buf, ',');
            ecs_strbuf_appendch(&buf, ' ');
        }

        if (id == 1) {
            ecs_strbuf_appendstr(&buf, "Component");
        } else {
            ecs_id_str_buf(world, id, &buf);
        }
    }

    return ecs_strbuf_get(&buf);
}

char* ecs_table_str(
    const ecs_world_t *world,
    const ecs_table_t *table)
{
    if (table) {
        return ecs_type_str(world, &table->type);
    } else {
        return NULL;
    }
}

static
void flecs_flush_bulk_new(
    ecs_world_t *world,
    ecs_defer_op_t *op)
{
    ecs_entity_t *entities = op->is._n.entities;

    if (op->id) {
        int i, count = op->is._n.count;
        for (i = 0; i < count; i ++) {
            add_id(world, entities[i], op->id);
        }
    }

    ecs_os_free(entities);
}

static
void flecs_dtor_value(
    ecs_world_t *world,
    ecs_id_t id,
    void *value,
    int32_t count)
{
    const ecs_type_info_t *ti = ecs_get_type_info(world, id);
    ecs_assert(ti != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_xtor_t dtor = ti->hooks.dtor;
    if (dtor) {
        ecs_size_t size = ti->size;
        void *ptr;
        int i;
        for (i = 0, ptr = value; i < count; i ++, ptr = ECS_OFFSET(ptr, size)) {
            dtor(ptr, 1, ti);
        }
    }
}

static
void flecs_discard_op(
    ecs_world_t *world,
    ecs_defer_op_t *op)
{
    if (op->kind != EcsOpBulkNew) {
        void *value = op->is._1.value;
        if (value) {
            flecs_dtor_value(world, op->id, value, 1);
            flecs_stack_free(value, op->is._1.size);
        }
    } else {
        ecs_os_free(op->is._n.entities);
    }
}

static 
bool flecs_is_entity_valid(
    ecs_world_t *world,
    ecs_entity_t e)
{
    if (ecs_exists(world, e) && !ecs_is_alive(world, e)) {
        return false;
    }
    return true;
}

static
bool flecs_remove_invalid(
    ecs_world_t *world,
    ecs_id_t *id_out)
{
    ecs_id_t id = *id_out;

    if (ECS_HAS_ROLE(id, PAIR)) {
        ecs_entity_t rel = ecs_pair_first(world, id);
        if (!rel || !flecs_is_entity_valid(world, rel)) {
            /* After relationship is deleted we can no longer see what its
             * delete action was, so pretend this never happened */
            *id_out = 0;
            return true;
        } else {
            ecs_entity_t obj = ecs_pair_second(world, id);
            if (!obj || !flecs_is_entity_valid(world, obj)) {
                /* Check the relationship's policy for deleted objects */
                ecs_id_record_t *idr = flecs_id_record_get(world, 
                    ecs_pair(rel, EcsWildcard));
                if (idr) {
                    ecs_entity_t action = ECS_ID_ON_DELETE_OBJECT(idr->flags);
                    if (action == EcsDelete) {
                        /* Entity should be deleted, don't bother checking
                         * other ids */
                        return false;
                    } else if (action == EcsPanic) {
                        /* If policy is throw this object should not have
                         * been deleted */
                        flecs_throw_invalid_delete(world, id);
                    } else {
                        *id_out = 0;
                        return true;
                    }
                } else {
                    *id_out = 0;
                    return true;
                }
            }
        }
    } else {
        id &= ECS_COMPONENT_MASK;
        if (!flecs_is_entity_valid(world, id)) {
            /* After relationship is deleted we can no longer see what its
             * delete action was, so pretend this never happened */
            *id_out = 0;
            return true;
        }
    }

    return true;
}

/* Leave safe section. Run all deferred commands. */
bool flecs_defer_end(
    ecs_world_t *world,
    ecs_stage_t *stage)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(stage != NULL, ECS_INVALID_PARAMETER, NULL);

    if (stage->defer_suspend) {
        return false;
    }

    if (!--stage->defer) {
        /* Set to NULL. Processing deferred commands can cause additional
         * commands to get enqueued (as result of reactive systems). Make sure
         * that the original array is not reallocated, as this would complicate
         * processing the queue. */
        ecs_vector_t *defer_queue = stage->defer_queue;
        stage->defer_queue = NULL;

        if (defer_queue) {
            ecs_defer_op_t *ops = ecs_vector_first(defer_queue, ecs_defer_op_t);
            int32_t i, count = ecs_vector_count(defer_queue);

            ecs_stack_t stack = stage->defer_stack;
            flecs_stack_init(&stage->defer_stack);

            for (i = 0; i < count; i ++) {
                ecs_defer_op_t *op = &ops[i];
                ecs_entity_t e = op->is._1.entity;
                if (op->kind == EcsOpBulkNew) {
                    e = 0;
                }

                /* If entity is no longer alive, this could be because the queue
                 * contained both a delete and a subsequent add/remove/set which
                 * should be ignored. */
                if (e && !ecs_is_alive(world, e) && flecs_entities_exists(world, e)) {
                    ecs_assert(op->kind != EcsOpNew && op->kind != EcsOpClone, 
                        ECS_INTERNAL_ERROR, NULL);
                    world->info.discard_count ++;
                    flecs_discard_op(world, op);
                    continue;
                }

                switch(op->kind) {
                case EcsOpNew:
                case EcsOpAdd:
                    ecs_assert(op->id != 0, ECS_INTERNAL_ERROR, NULL);
                    if (flecs_remove_invalid(world, &op->id)) {
                        if (op->id) {
                            world->info.add_count ++;
                            add_id(world, e, op->id);
                        }
                    } else {
                        ecs_delete(world, e);
                    }
                    break;
                case EcsOpRemove:
                    remove_id(world, e, op->id);
                    break;
                case EcsOpClone:
                    ecs_clone(world, e, op->id, op->is._1.clone_value);
                    break;
                case EcsOpSet:
                    set_ptr_w_id(world, e, 
                        op->id, flecs_itosize(op->is._1.size), 
                        op->is._1.value, true, true);
                    break;
                case EcsOpMut:
                    set_ptr_w_id(world, e, 
                        op->id, flecs_itosize(op->is._1.size), 
                        op->is._1.value, true, false);
                    break;
                case EcsOpModified:
                    if (ecs_has_id(world, e, op->id)) {
                        ecs_modified_id(world, e, op->id);
                    }
                    break;
                case EcsOpDelete: {
                    ecs_delete(world, e);
                    break;
                }
                case EcsOpClear:
                    ecs_clear(world, e);
                    break;
                case EcsOpOnDeleteAction:
                    flecs_on_delete(world, op->id, e);
                    break;
                case EcsOpEnable:
                    ecs_enable_component_w_id(world, e, op->id, true);
                    break;
                case EcsOpDisable:
                    ecs_enable_component_w_id(world, e, op->id, false);
                    break;
                case EcsOpBulkNew:
                    flecs_flush_bulk_new(world, op);
                    continue;
                }

                if (op->is._1.value) {
                    flecs_stack_free(op->is._1.value, op->is._1.size);
                }
            }

            if (stage->defer_queue) {
                ecs_vector_free(stage->defer_queue);
            }

            /* Restore defer queue */
            ecs_vector_clear(defer_queue);
            stage->defer_queue = defer_queue;

            /* Restore stack */
            flecs_stack_fini(&stage->defer_stack);
            stage->defer_stack = stack;
            flecs_stack_reset(&stage->defer_stack);
        }

        return true;
    }

error:
    return false;
}

/* Delete operations from queue without executing them. */
bool flecs_defer_purge(
    ecs_world_t *world,
    ecs_stage_t *stage)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(stage != NULL, ECS_INVALID_PARAMETER, NULL);

    if (!--stage->defer) {
        ecs_vector_t *defer_queue = stage->defer_queue;
        stage->defer_queue = NULL;

        if (defer_queue) {
            ecs_defer_op_t *ops = ecs_vector_first(defer_queue, ecs_defer_op_t);
            int32_t i, count = ecs_vector_count(defer_queue);
            for (i = 0; i < count; i ++) {
                flecs_discard_op(world, &ops[i]);
            }

            if (stage->defer_queue) {
                ecs_vector_free(stage->defer_queue);
            }

            /* Restore defer queue */
            ecs_vector_clear(defer_queue);
            stage->defer_queue = defer_queue;
            flecs_stack_reset(&stage->defer_stack);
        }

        return true;
    }

error:
    return false;
}


static
ecs_defer_op_t* new_defer_op(ecs_stage_t *stage) {
    ecs_defer_op_t *result = ecs_vector_add(&stage->defer_queue, ecs_defer_op_t);
    ecs_os_memset(result, 0, ECS_SIZEOF(ecs_defer_op_t));
    return result;
}

static
void merge_stages(
    ecs_world_t *world,
    bool force_merge)
{
    bool is_stage = ecs_poly_is(world, ecs_stage_t);
    ecs_stage_t *stage = flecs_stage_from_world(&world);

    bool measure_frame_time = ECS_BIT_IS_SET(world->flags, 
        EcsWorldMeasureFrameTime);

    ecs_time_t t_start;
    if (measure_frame_time) {
        ecs_os_get_time(&t_start);
    }

    ecs_dbg_3("#[magenta]merge");

    if (is_stage) {
        /* Check for consistency if force_merge is enabled. In practice this
         * function will never get called with force_merge disabled for just
         * a single stage. */
        if (force_merge || stage->auto_merge) {
            ecs_assert(stage->defer == 1, ECS_INVALID_OPERATION, 
                "mismatching defer_begin/defer_end detected");
            ecs_defer_end((ecs_world_t*)stage);
        }
    } else {
        /* Merge stages. Only merge if the stage has auto_merging turned on, or 
         * if this is a forced merge (like when ecs_merge is called) */
        int32_t i, count = ecs_get_stage_count(world);
        for (i = 0; i < count; i ++) {
            ecs_stage_t *s = (ecs_stage_t*)ecs_get_stage(world, i);
            ecs_poly_assert(s, ecs_stage_t);
            if (force_merge || s->auto_merge) {
                flecs_defer_end(world, s);
            }
        }
    }

    flecs_eval_component_monitors(world);

    if (measure_frame_time) {
        world->info.merge_time_total += (float)ecs_time_measure(&t_start);
    }

    world->info.merge_count_total ++; 

    /* If stage is asynchronous, deferring is always enabled */
    if (stage->asynchronous) {
        ecs_defer_begin((ecs_world_t*)stage);
    }
}

static
void do_auto_merge(
    ecs_world_t *world)
{
    merge_stages(world, false);
}

static
void do_manual_merge(
    ecs_world_t *world)
{
    merge_stages(world, true);
}

bool flecs_defer_begin(
    ecs_world_t *world,
    ecs_stage_t *stage)
{
    (void)world;
    if (stage->defer_suspend) return false;
    return (++ stage->defer) == 1;
}

static
bool flecs_defer_op(
    ecs_world_t *world,
    ecs_stage_t *stage)
{
    (void)world;

    /* If deferring is suspended, do operation as usual */
    if (stage->defer_suspend) return false;

    if (stage->defer) {
        /* If deferring is enabled, defer operation */
        return true;
    }

    /* Deferring is disabled, defer while operation is executed */
    stage->defer ++;
    return false;
}

static
bool flecs_defer_add_remove(
    ecs_world_t *world,
    ecs_stage_t *stage,
    ecs_defer_op_kind_t op_kind,
    ecs_entity_t entity,
    ecs_id_t id)
{
    if (flecs_defer_op(world, stage)) {
        if (!id) {
            return true;
        }

        ecs_defer_op_t *op = new_defer_op(stage);
        op->kind = op_kind;
        op->id = id;
        op->is._1.entity = entity;

        if (op_kind == EcsOpNew) {
            world->info.new_count ++;
        } else if (op_kind == EcsOpAdd) {
            world->info.add_count ++;
        } else if (op_kind == EcsOpRemove) {
            world->info.remove_count ++;
        }

        return true;
    }
    return false;
}

bool flecs_defer_modified(
    ecs_world_t *world,
    ecs_stage_t *stage,
    ecs_entity_t entity,
    ecs_id_t id)
{
    if (flecs_defer_op(world, stage)) {
        ecs_defer_op_t *op = new_defer_op(stage);
        op->kind = EcsOpModified;
        op->id = id;
        op->is._1.entity = entity;
        return true;
    }
    return false;
}

bool flecs_defer_clone(
    ecs_world_t *world,
    ecs_stage_t *stage,
    ecs_entity_t entity,
    ecs_entity_t src,
    bool clone_value)
{   
    if (flecs_defer_op(world, stage)) {
        ecs_defer_op_t *op = new_defer_op(stage);
        op->kind = EcsOpClone;
        op->id = src;
        op->is._1.entity = entity;
        op->is._1.clone_value = clone_value;
        return true;
    }
    return false;   
}

bool flecs_defer_delete(
    ecs_world_t *world,
    ecs_stage_t *stage,
    ecs_entity_t entity)
{
    if (flecs_defer_op(world, stage)) {
        ecs_defer_op_t *op = new_defer_op(stage);
        op->kind = EcsOpDelete;
        op->is._1.entity = entity;
        world->info.delete_count ++;
        return true;
    }
    return false;
}

bool flecs_defer_clear(
    ecs_world_t *world,
    ecs_stage_t *stage,
    ecs_entity_t entity)
{
    if (flecs_defer_op(world, stage)) {
        ecs_defer_op_t *op = new_defer_op(stage);
        op->kind = EcsOpClear;
        op->is._1.entity = entity;
        world->info.clear_count ++;
        return true;
    }
    return false;
}

bool flecs_defer_on_delete_action(
    ecs_world_t *world,
    ecs_stage_t *stage,
    ecs_id_t id,
    ecs_entity_t action)
{
    if (flecs_defer_op(world, stage)) {
        ecs_defer_op_t *op = new_defer_op(stage);
        op->kind = EcsOpOnDeleteAction;
        op->id = id;
        op->is._1.entity = action;
        world->info.clear_count ++;
        return true;
    }
    return false;
}

bool flecs_defer_enable(
    ecs_world_t *world,
    ecs_stage_t *stage,
    ecs_entity_t entity,
    ecs_id_t id,
    bool enable)
{
    if (flecs_defer_op(world, stage)) {
        ecs_defer_op_t *op = new_defer_op(stage);
        op->kind = enable ? EcsOpEnable : EcsOpDisable;
        op->is._1.entity = entity;
        op->id = id;
        return true;
    }
    return false;
}

bool flecs_defer_bulk_new(
    ecs_world_t *world,
    ecs_stage_t *stage,
    int32_t count,
    ecs_id_t id,
    const ecs_entity_t **ids_out)
{
    if (flecs_defer_op(world, stage)) {
        ecs_entity_t *ids = ecs_os_malloc(count * ECS_SIZEOF(ecs_entity_t));
        world->info.bulk_new_count ++;

        /* Use ecs_new_id as this is thread safe */
        int i;
        for (i = 0; i < count; i ++) {
            ids[i] = ecs_new_id(world);
        }

        *ids_out = ids;

        /* Store data in op */
        ecs_defer_op_t *op = new_defer_op(stage);
        op->kind = EcsOpBulkNew;
        op->id = id;
        op->is._n.entities = ids;
        op->is._n.count = count;

        return true;
    }
    return false;
}

bool flecs_defer_new(
    ecs_world_t *world,
    ecs_stage_t *stage,
    ecs_entity_t entity,
    ecs_id_t id)
{   
    return flecs_defer_add_remove(world, stage, EcsOpNew, entity, id);
}

bool flecs_defer_add(
    ecs_world_t *world,
    ecs_stage_t *stage,
    ecs_entity_t entity,
    ecs_id_t id)
{   
    return flecs_defer_add_remove(world, stage, EcsOpAdd, entity, id);
}

bool flecs_defer_remove(
    ecs_world_t *world,
    ecs_stage_t *stage,
    ecs_entity_t entity,
    ecs_id_t id)
{
    return flecs_defer_add_remove(world, stage, EcsOpRemove, entity, id);
}

bool flecs_defer_set(
    ecs_world_t *world,
    ecs_stage_t *stage,
    ecs_defer_op_kind_t op_kind,
    ecs_entity_t entity,
    ecs_id_t id,
    ecs_size_t size,
    const void *value,
    void **value_out)
{
    if (flecs_defer_op(world, stage)) {
        world->info.set_count ++;

        ecs_id_record_t *idr = flecs_id_record_ensure(world, id);
        ecs_check(idr != NULL && idr->type_info != NULL, 
            ECS_INVALID_PARAMETER, NULL);
        
        const ecs_type_info_t *ti = idr->type_info;
        ecs_assert(!size || size == ti->size, ECS_INVALID_PARAMETER, NULL);
        size = ti->size;

        ecs_stack_t *stack = &stage->defer_stack;
        ecs_defer_op_t *op = new_defer_op(stage);
        op->kind = op_kind;
        op->id = id;
        op->is._1.entity = entity;
        op->is._1.size = size;
        op->is._1.value = flecs_stack_alloc(stack, size, ti->alignment);

        if (!value) {
            value = ecs_get_id(world, entity, id);
        }

        if (value) {
            ecs_copy_t copy;
            if ((copy = ti->hooks.copy_ctor)) {
                copy(op->is._1.value, value, 1, ti);
            } else {
                ecs_os_memcpy(op->is._1.value, value, size);
            }
        } else {
            ecs_xtor_t ctor;
            if ((ctor = ti->hooks.ctor)) {
                ctor(op->is._1.value, 1, ti);
            }
        }

        if (value_out) {
            *value_out = op->is._1.value;
        }

        return true;
    }
error:
    return false;
}

void flecs_stage_merge_post_frame(
    ecs_world_t *world,
    ecs_stage_t *stage)
{
    /* Execute post frame actions */
    ecs_vector_each(stage->post_frame_actions, ecs_action_elem_t, action, {
        action->action(world, action->ctx);
    });

    ecs_vector_free(stage->post_frame_actions);
    stage->post_frame_actions = NULL;
}

void flecs_stage_init(
    ecs_world_t *world,
    ecs_stage_t *stage)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_poly_init(stage, ecs_stage_t);

    stage->world = world;
    stage->thread_ctx = world;
    stage->auto_merge = true;
    stage->asynchronous = false;
    
    flecs_stack_init(&stage->defer_stack);
}

void flecs_stage_deinit(
    ecs_world_t *world,
    ecs_stage_t *stage)
{
    (void)world;
    ecs_poly_assert(world, ecs_world_t);
    ecs_poly_assert(stage, ecs_stage_t);

    /* Make sure stage has no unmerged data */
    ecs_assert(ecs_vector_count(stage->defer_queue) == 0, 
        ECS_INTERNAL_ERROR, NULL);

    ecs_poly_fini(stage, ecs_stage_t);

    ecs_vector_free(stage->defer_queue);
    flecs_stack_fini(&stage->defer_stack);
}

void ecs_set_stage_count(
    ecs_world_t *world,
    int32_t stage_count)
{
    ecs_poly_assert(world, ecs_world_t);

    /* World must have at least one default stage */
    ecs_assert(stage_count >= 1 || (world->flags & EcsWorldFini), 
        ECS_INTERNAL_ERROR, NULL);

    bool auto_merge = true;
    ecs_entity_t *lookup_path = NULL;
    ecs_entity_t scope = 0;
    ecs_entity_t with = 0;
    if (world->stage_count >= 1) {
        auto_merge = world->stages[0].auto_merge;
        lookup_path = world->stages[0].lookup_path;
        scope = world->stages[0].scope;
        with = world->stages[0].with;
    }

    int32_t i, count = world->stage_count;
    if (count && count != stage_count) {
        ecs_stage_t *stages = world->stages;

        for (i = 0; i < count; i ++) {
            /* If stage contains a thread handle, ecs_set_threads was used to
             * create the stages. ecs_set_threads and ecs_set_stage_count should not
             * be mixed. */
            ecs_poly_assert(&stages[i], ecs_stage_t);
            ecs_check(stages[i].thread == 0, ECS_INVALID_OPERATION, NULL);
            flecs_stage_deinit(world, &stages[i]);
        }

        ecs_os_free(world->stages);
    }

    if (stage_count) {
        world->stages = ecs_os_malloc_n(ecs_stage_t, stage_count);

        for (i = 0; i < stage_count; i ++) {
            ecs_stage_t *stage = &world->stages[i];
            flecs_stage_init(world, stage);
            stage->id = i;

            /* Set thread_ctx to stage, as this stage might be used in a
             * multithreaded context */
            stage->thread_ctx = (ecs_world_t*)stage;
        }
    } else {
        /* Set to NULL to prevent double frees */
        world->stages = NULL;
    }

    /* Regardless of whether the stage was just initialized or not, when the
     * ecs_set_stage_count function is called, all stages inherit the auto_merge
     * property from the world */
    for (i = 0; i < stage_count; i ++) {
        world->stages[i].auto_merge = auto_merge;
        world->stages[i].lookup_path = lookup_path;
        world->stages[0].scope = scope;
        world->stages[0].with = with;
    }

    world->stage_count = stage_count;
error:
    return;
}

int32_t ecs_get_stage_count(
    const ecs_world_t *world)
{
    world = ecs_get_world(world);
    return world->stage_count;
}

int32_t ecs_get_stage_id(
    const ecs_world_t *world)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);

    if (ecs_poly_is(world, ecs_stage_t)) {
        ecs_stage_t *stage = (ecs_stage_t*)world;

        /* Index 0 is reserved for main stage */
        return stage->id;
    } else if (ecs_poly_is(world, ecs_world_t)) {
        return 0;
    } else {
        ecs_throw(ECS_INTERNAL_ERROR, NULL);
    }
error:
    return 0;
}

ecs_world_t* ecs_get_stage(
    const ecs_world_t *world,
    int32_t stage_id)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_check(world->stage_count > stage_id, ECS_INVALID_PARAMETER, NULL);
    return (ecs_world_t*)&world->stages[stage_id];
error:
    return NULL;
}

bool ecs_readonly_begin(
    ecs_world_t *world)
{
    ecs_poly_assert(world, ecs_world_t);

    flecs_process_pending_tables(world);

    ecs_dbg_3("#[bold]readonly");
    ecs_log_push_3();

    int32_t i, count = ecs_get_stage_count(world);
    for (i = 0; i < count; i ++) {
        ecs_stage_t *stage = &world->stages[i];
        stage->lookup_path = world->stages[0].lookup_path;
        ecs_assert(stage->defer == 0, ECS_INVALID_OPERATION, 
            "deferred mode cannot be enabled when entering readonly mode");
        ecs_defer_begin((ecs_world_t*)stage);
    }

    bool is_readonly = ECS_BIT_IS_SET(world->flags, EcsWorldReadonly);

    /* From this point on, the world is "locked" for mutations, and it is only 
     * allowed to enqueue commands from stages */
    ECS_BIT_SET(world->flags, EcsWorldReadonly);

    return is_readonly;
}

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

    /* After this it is safe again to mutate the world directly */
    ECS_BIT_CLEAR(world->flags, EcsWorldReadonly);

    ecs_log_pop_3();

    do_auto_merge(world);
error:
    return;
}

void ecs_merge(
    ecs_world_t *world)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(ecs_poly_is(world, ecs_world_t) || 
               ecs_poly_is(world, ecs_stage_t), ECS_INVALID_PARAMETER, NULL);
    do_manual_merge(world);
error:
    return;
}

void ecs_set_automerge(
    ecs_world_t *world,
    bool auto_merge)
{
    /* If a world is provided, set auto_merge globally for the world. This
     * doesn't actually do anything (the main stage never merges) but it serves
     * as the default for when stages are created. */
    if (ecs_poly_is(world, ecs_world_t)) {
        world->stages[0].auto_merge = auto_merge;

        /* Propagate change to all stages */
        int i, stage_count = ecs_get_stage_count(world);
        for (i = 0; i < stage_count; i ++) {
            ecs_stage_t *stage = (ecs_stage_t*)ecs_get_stage(world, i);
            stage->auto_merge = auto_merge;
        }

    /* If a stage is provided, override the auto_merge value for the individual
     * stage. This allows an application to control per-stage which stage should
     * be automatically merged and which one shouldn't */
    } else {
        ecs_poly_assert(world, ecs_stage_t);
        ecs_stage_t *stage = (ecs_stage_t*)world;
        stage->auto_merge = auto_merge;
    }
}

bool ecs_stage_is_readonly(
    const ecs_world_t *stage)
{
    const ecs_world_t *world = ecs_get_world(stage);

    if (ecs_poly_is(stage, ecs_stage_t)) {
        if (((ecs_stage_t*)stage)->asynchronous) {
            return false;
        }
    }

    if (world->flags & EcsWorldReadonly) {
        if (ecs_poly_is(stage, ecs_world_t)) {
            return true;
        }
    } else {
        if (ecs_poly_is(stage, ecs_stage_t)) {
            return true;
        }
    }

    return false;
}

ecs_world_t* ecs_async_stage_new(
    ecs_world_t *world)
{
    ecs_stage_t *stage = ecs_os_calloc(sizeof(ecs_stage_t));
    flecs_stage_init(world, stage);

    stage->id = -1;
    stage->auto_merge = false;
    stage->asynchronous = true;

    ecs_defer_begin((ecs_world_t*)stage);

    return (ecs_world_t*)stage;
}

void ecs_async_stage_free(
    ecs_world_t *world)
{
    ecs_poly_assert(world, ecs_stage_t);
    ecs_stage_t *stage = (ecs_stage_t*)world;
    ecs_check(stage->asynchronous == true, ECS_INVALID_PARAMETER, NULL);
    flecs_stage_deinit(stage->world, stage);
    ecs_os_free(stage);
error:
    return;
}

bool ecs_stage_is_async(
    ecs_world_t *stage)
{
    if (!stage) {
        return false;
    }
    
    if (!ecs_poly_is(stage, ecs_stage_t)) {
        return false;
    }

    return ((ecs_stage_t*)stage)->asynchronous;
}

bool ecs_is_deferred(
    const ecs_world_t *world)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    const ecs_stage_t *stage = flecs_stage_from_readonly_world(world);
    return stage->defer != 0;
error:
    return false;
}


struct ecs_vector_t {
    int32_t count;
    int32_t size;
    
#ifndef FLECS_NDEBUG
    int64_t elem_size; /* Used in debug mode to validate size */
#endif
};

/** Resize the vector buffer */
static
ecs_vector_t* resize(
    ecs_vector_t *vector,
    int16_t offset,
    int32_t size)
{
    ecs_vector_t *result = ecs_os_realloc(vector, offset + size);
    ecs_assert(result != NULL, ECS_OUT_OF_MEMORY, 0);
    return result;
}

/* -- Public functions -- */

ecs_vector_t* _ecs_vector_new(
    ecs_size_t elem_size,
    int16_t offset,
    int32_t elem_count)
{
    ecs_assert(elem_size != 0, ECS_INTERNAL_ERROR, NULL);
    
    ecs_vector_t *result =
        ecs_os_malloc(offset + elem_size * elem_count);
    ecs_assert(result != NULL, ECS_OUT_OF_MEMORY, NULL);

    result->count = 0;
    result->size = elem_count;
#ifndef FLECS_NDEBUG
    result->elem_size = elem_size;
#endif
    return result;
}

ecs_vector_t* _ecs_vector_from_array(
    ecs_size_t elem_size,
    int16_t offset,
    int32_t elem_count,
    void *array)
{
    ecs_assert(elem_size != 0, ECS_INTERNAL_ERROR, NULL);
    
    ecs_vector_t *result =
        ecs_os_malloc(offset + elem_size * elem_count);
    ecs_assert(result != NULL, ECS_OUT_OF_MEMORY, NULL);

    ecs_os_memcpy(ECS_OFFSET(result, offset), array, elem_size * elem_count);

    result->count = elem_count;
    result->size = elem_count;
#ifndef FLECS_NDEBUG
    result->elem_size = elem_size;
#endif
    return result;   
}

void ecs_vector_free(
    ecs_vector_t *vector)
{
    ecs_os_free(vector);
}

void ecs_vector_clear(
    ecs_vector_t *vector)
{
    if (vector) {
        vector->count = 0;
    }
}

void _ecs_vector_zero(
    ecs_vector_t *vector,
    ecs_size_t elem_size,
    int16_t offset)
{
    void *array = ECS_OFFSET(vector, offset);
    ecs_os_memset(array, 0, elem_size * vector->count);
}

void ecs_vector_assert_size(
    ecs_vector_t *vector,
    ecs_size_t elem_size)
{
    (void)elem_size;
    
    if (vector) {
        ecs_dbg_assert(vector->elem_size == elem_size, ECS_INTERNAL_ERROR, NULL);
    }
}

void* _ecs_vector_addn(
    ecs_vector_t **array_inout,
    ecs_size_t elem_size,
    int16_t offset,
    int32_t elem_count)
{
    ecs_assert(array_inout != NULL, ECS_INTERNAL_ERROR, NULL);
    
    if (elem_count == 1) {
        return _ecs_vector_add(array_inout, elem_size, offset);
    }
    
    ecs_vector_t *vector = *array_inout;
    if (!vector) {
        vector = _ecs_vector_new(elem_size, offset, 1);
        *array_inout = vector;
    }

    ecs_dbg_assert(vector->elem_size == elem_size, ECS_INTERNAL_ERROR, NULL);

    int32_t max_count = vector->size;
    int32_t old_count = vector->count;
    int32_t new_count = old_count + elem_count;

    if ((new_count - 1) >= max_count) {
        if (!max_count) {
            max_count = elem_count;
        } else {
            while (max_count < new_count) {
                max_count *= 2;
            }
        }

        max_count = flecs_next_pow_of_2(max_count);
        vector = resize(vector, offset, max_count * elem_size);
        vector->size = max_count;
        *array_inout = vector;
    }

    vector->count = new_count;

    return ECS_OFFSET(vector, offset + elem_size * old_count);
}

void* _ecs_vector_add(
    ecs_vector_t **array_inout,
    ecs_size_t elem_size,
    int16_t offset)
{
    ecs_assert(array_inout != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_vector_t *vector = *array_inout;
    int32_t count, size;

    if (vector) {
        ecs_dbg_assert(vector->elem_size == elem_size, ECS_INTERNAL_ERROR, NULL);
        count = vector->count;
        size = vector->size;

        if (count >= size) {
            size *= 2;
            if (!size) {
                size = 2;
            }

            size = flecs_next_pow_of_2(size);
            vector = resize(vector, offset, size * elem_size);
            *array_inout = vector;
            vector->size = size;
        }

        vector->count = count + 1;
        return ECS_OFFSET(vector, offset + elem_size * count);
    }

    vector = _ecs_vector_new(elem_size, offset, 2);
    *array_inout = vector;
    vector->count = 1;
    vector->size = 2;
    return ECS_OFFSET(vector, offset);
}

void* _ecs_vector_insert_at(
    ecs_vector_t **vec,
    ecs_size_t elem_size,
    int16_t offset,
    int32_t index)
{
    ecs_assert(vec != NULL, ECS_INTERNAL_ERROR, NULL);
    int32_t count = vec[0]->count;
    if (index == count) {
        return _ecs_vector_add(vec, elem_size, offset);
    }
    ecs_assert(index < count, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(index >= 0, ECS_INTERNAL_ERROR, NULL);

    _ecs_vector_add(vec, elem_size, offset);
    void *start = _ecs_vector_get(*vec, elem_size, offset, index);
    if (index < count) {
        ecs_os_memmove(ECS_OFFSET(start, elem_size), start, 
            (count - index) * elem_size);
    }

    return start;
}

int32_t _ecs_vector_move_index(
    ecs_vector_t **dst,
    ecs_vector_t *src,
    ecs_size_t elem_size,
    int16_t offset,
    int32_t index)
{
    if (dst && *dst) {
        ecs_dbg_assert((*dst)->elem_size == elem_size, ECS_INTERNAL_ERROR, NULL);
    }
    ecs_dbg_assert(src->elem_size == elem_size, ECS_INTERNAL_ERROR, NULL);

    void *dst_elem = _ecs_vector_add(dst, elem_size, offset);
    void *src_elem = _ecs_vector_get(src, elem_size, offset, index);

    ecs_os_memcpy(dst_elem, src_elem, elem_size);
    return _ecs_vector_remove(src, elem_size, offset, index);
}

void ecs_vector_remove_last(
    ecs_vector_t *vector)
{
    if (vector && vector->count) vector->count --;
}

bool _ecs_vector_pop(
    ecs_vector_t *vector,
    ecs_size_t elem_size,
    int16_t offset,
    void *value)
{
    if (!vector) {
        return false;
    }

    ecs_dbg_assert(vector->elem_size == elem_size, ECS_INTERNAL_ERROR, NULL);

    int32_t count = vector->count;
    if (!count) {
        return false;
    }

    void *elem = ECS_OFFSET(vector, offset + (count - 1) * elem_size);

    if (value) {
        ecs_os_memcpy(value, elem, elem_size);
    }

    ecs_vector_remove_last(vector);

    return true;
}

int32_t _ecs_vector_remove(
    ecs_vector_t *vector,
    ecs_size_t elem_size,
    int16_t offset,
    int32_t index)
{
    ecs_dbg_assert(vector->elem_size == elem_size, ECS_INTERNAL_ERROR, NULL);
    
    int32_t count = vector->count;
    void *buffer = ECS_OFFSET(vector, offset);
    void *elem = ECS_OFFSET(buffer, index * elem_size);

    ecs_assert(index < count, ECS_INVALID_PARAMETER, NULL);

    count --;
    if (index != count) {
        void *last_elem = ECS_OFFSET(buffer, elem_size * count);
        ecs_os_memcpy(elem, last_elem, elem_size);
    }

    vector->count = count;

    return count;
}

void _ecs_vector_reclaim(
    ecs_vector_t **array_inout,
    ecs_size_t elem_size,
    int16_t offset)
{
    ecs_vector_t *vector = *array_inout;
    if (!vector) {
        return;
    }

    ecs_dbg_assert(vector->elem_size == elem_size, ECS_INTERNAL_ERROR, NULL);
    
    int32_t size = vector->size;
    int32_t count = vector->count;

    if (count < size) {
        if (count) {
            size = count;
            vector = resize(vector, offset, size * elem_size);
            vector->size = size;
            *array_inout = vector;
        } else {
            ecs_vector_free(vector);
            *array_inout = NULL;
        }
    }
}

int32_t ecs_vector_count(
    const ecs_vector_t *vector)
{
    if (!vector) {
        return 0;
    }
    return vector->count;
}

int32_t ecs_vector_size(
    const ecs_vector_t *vector)
{
    if (!vector) {
        return 0;
    }
    return vector->size;
}

int32_t _ecs_vector_set_size(
    ecs_vector_t **array_inout,
    ecs_size_t elem_size,
    int16_t offset,
    int32_t elem_count)
{
    ecs_vector_t *vector = *array_inout;

    if (!vector) {
        *array_inout = _ecs_vector_new(elem_size, offset, elem_count);
        return elem_count;
    } else {
        ecs_dbg_assert(vector->elem_size == elem_size, ECS_INTERNAL_ERROR, NULL);

        int32_t result = vector->size;

        if (elem_count < vector->count) {
            elem_count = vector->count;
        }

        if (result < elem_count) {
            elem_count = flecs_next_pow_of_2(elem_count);
            vector = resize(vector, offset, elem_count * elem_size);
            vector->size = elem_count;
            *array_inout = vector;
            result = elem_count;
        }

        return result;
    }
}

int32_t _ecs_vector_grow(
    ecs_vector_t **array_inout,
    ecs_size_t elem_size,
    int16_t offset,
    int32_t elem_count)
{
    int32_t current = ecs_vector_count(*array_inout);
    return _ecs_vector_set_size(array_inout, elem_size, offset, current + elem_count);
}

int32_t _ecs_vector_set_count(
    ecs_vector_t **array_inout,
    ecs_size_t elem_size,
    int16_t offset,
    int32_t elem_count)
{
    if (!*array_inout) {
        *array_inout = _ecs_vector_new(elem_size, offset, elem_count);
    }

    ecs_dbg_assert((*array_inout)->elem_size == elem_size, ECS_INTERNAL_ERROR, NULL);

    (*array_inout)->count = elem_count;
    ecs_size_t size = _ecs_vector_set_size(array_inout, elem_size, offset, elem_count);
    return size;
}

void* _ecs_vector_first(
    const ecs_vector_t *vector,
    ecs_size_t elem_size,
    int16_t offset)
{
    (void)elem_size;

    ecs_dbg_assert(!vector || vector->elem_size == elem_size, ECS_INTERNAL_ERROR, NULL);
    if (vector && vector->size) {
        return ECS_OFFSET(vector, offset);
    } else {
        return NULL;
    }
}

void* _ecs_vector_get(
    const ecs_vector_t *vector,
    ecs_size_t elem_size,
    int16_t offset,
    int32_t index)
{
    ecs_assert(vector != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_dbg_assert(vector->elem_size == elem_size, ECS_INTERNAL_ERROR, NULL);    
    ecs_assert(index >= 0, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(index < vector->count, ECS_INTERNAL_ERROR, NULL);

    return ECS_OFFSET(vector, offset + elem_size * index);
}

void* _ecs_vector_last(
    const ecs_vector_t *vector,
    ecs_size_t elem_size,
    int16_t offset)
{
    if (vector) {
        ecs_dbg_assert(vector->elem_size == elem_size, ECS_INTERNAL_ERROR, NULL);
        int32_t count = vector->count;
        if (!count) {
            return NULL;
        } else {
            return ECS_OFFSET(vector, offset + elem_size * (count - 1));
        }
    } else {
        return NULL;
    }
}

int32_t _ecs_vector_set_min_size(
    ecs_vector_t **vector_inout,
    ecs_size_t elem_size,
    int16_t offset,
    int32_t elem_count)
{
    if (!*vector_inout || (*vector_inout)->size < elem_count) {
        return _ecs_vector_set_size(vector_inout, elem_size, offset, elem_count);
    } else {
        return (*vector_inout)->size;
    }
}

int32_t _ecs_vector_set_min_count(
    ecs_vector_t **vector_inout,
    ecs_size_t elem_size,
    int16_t offset,
    int32_t elem_count)
{
    _ecs_vector_set_min_size(vector_inout, elem_size, offset, elem_count);

    ecs_vector_t *v = *vector_inout;
    if (v && v->count < elem_count) {
        v->count = elem_count;
    }

    return v->count;
}

void _ecs_vector_sort(
    ecs_vector_t *vector,
    ecs_size_t elem_size,
    int16_t offset,
    ecs_comparator_t compare_action)
{
    if (!vector) {
        return;
    }

    ecs_dbg_assert(vector->elem_size == elem_size, ECS_INTERNAL_ERROR, NULL);    

    int32_t count = vector->count;
    void *buffer = ECS_OFFSET(vector, offset);

    if (count > 1) {
        qsort(buffer, (size_t)count, (size_t)elem_size, compare_action);
    }
}

void _ecs_vector_memory(
    const ecs_vector_t *vector,
    ecs_size_t elem_size,
    int16_t offset,
    int32_t *allocd,
    int32_t *used)
{
    if (!vector) {
        return;
    }

    ecs_dbg_assert(vector->elem_size == elem_size, ECS_INTERNAL_ERROR, NULL);

    if (allocd) {
        *allocd += vector->size * elem_size + offset;
    }
    if (used) {
        *used += vector->count * elem_size;
    }
}

ecs_vector_t* _ecs_vector_copy(
    const ecs_vector_t *src,
    ecs_size_t elem_size,
    int16_t offset)
{
    if (!src) {
        return NULL;
    }

    ecs_vector_t *dst = _ecs_vector_new(elem_size, offset, src->size);
    ecs_os_memcpy(dst, src, offset + elem_size * src->count);
    return dst;
}


/** The number of elements in a single chunk */
#define CHUNK_COUNT (4096)

/** Compute the chunk index from an id by stripping the first 12 bits */
#define CHUNK(index) ((int32_t)((uint32_t)index >> 12))

/** This computes the offset of an index inside a chunk */
#define OFFSET(index) ((int32_t)index & 0xFFF)

/* Utility to get a pointer to the payload */
#define DATA(array, size, offset) (ECS_OFFSET(array, size * offset))

typedef struct chunk_t {
    int32_t *sparse;            /* Sparse array with indices to dense array */
    void *data;                 /* Store data in sparse array to reduce  
                                 * indirection and provide stable pointers. */
} chunk_t;

static
chunk_t* chunk_new(
    ecs_sparse_t *sparse,
    int32_t chunk_index)
{
    int32_t count = ecs_vector_count(sparse->chunks);
    chunk_t *chunks;

    if (count <= chunk_index) {
        ecs_vector_set_count(&sparse->chunks, chunk_t, chunk_index + 1);
        chunks = ecs_vector_first(sparse->chunks, chunk_t);
        ecs_os_memset(&chunks[count], 0, (1 + chunk_index - count) * ECS_SIZEOF(chunk_t));
    } else {
        chunks = ecs_vector_first(sparse->chunks, chunk_t);
    }

    ecs_assert(chunks != NULL, ECS_INTERNAL_ERROR, NULL);

    chunk_t *result = &chunks[chunk_index];
    ecs_assert(result->sparse == NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(result->data == NULL, ECS_INTERNAL_ERROR, NULL);

    /* Initialize sparse array with zero's, as zero is used to indicate that the
     * sparse element has not been paired with a dense element. Use zero
     * as this means we can take advantage of calloc having a possibly better 
     * performance than malloc + memset. */
    result->sparse = ecs_os_calloc(ECS_SIZEOF(int32_t) * CHUNK_COUNT);

    /* Initialize the data array with zero's to guarantee that data is 
     * always initialized. When an entry is removed, data is reset back to
     * zero. Initialize now, as this can take advantage of calloc. */
    result->data = ecs_os_calloc(sparse->size * CHUNK_COUNT);

    ecs_assert(result->sparse != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(result->data != NULL, ECS_INTERNAL_ERROR, NULL);

    return result;
}

static
void chunk_free(
    chunk_t *chunk)
{
    ecs_os_free(chunk->sparse);
    ecs_os_free(chunk->data);
}

static
chunk_t* get_chunk(
    const ecs_sparse_t *sparse,
    int32_t chunk_index)
{
    if (!sparse->chunks) {
        return NULL;
    }
    if (chunk_index >= ecs_vector_count(sparse->chunks)) {
        return NULL;
    }

    /* If chunk_index is below zero, application used an invalid entity id */
    ecs_assert(chunk_index >= 0, ECS_INVALID_PARAMETER, NULL);
    chunk_t *result = ecs_vector_get(sparse->chunks, chunk_t, chunk_index);
    if (result && !result->sparse) {
        return NULL;
    }

    return result;
}

static
chunk_t* get_or_create_chunk(
    ecs_sparse_t *sparse,
    int32_t chunk_index)
{
    chunk_t *chunk = get_chunk(sparse, chunk_index);
    if (chunk) {
        return chunk;
    }

    return chunk_new(sparse, chunk_index);
}

static
void grow_dense(
    ecs_sparse_t *sparse)
{
    ecs_vector_add(&sparse->dense, uint64_t);
}

static
uint64_t strip_generation(
    uint64_t *index_out)
{
    uint64_t index = *index_out;
    uint64_t gen = index & ECS_GENERATION_MASK;
    /* Make sure there's no junk in the id */
    ecs_assert(gen == (index & (0xFFFFFFFFull << 32)),
        ECS_INVALID_PARAMETER, NULL);
    *index_out -= gen;
    return gen;
}

static
void assign_index(
    chunk_t * chunk, 
    uint64_t * dense_array, 
    uint64_t index, 
    int32_t dense)
{
    /* Initialize sparse-dense pair. This assigns the dense index to the sparse
     * array, and the sparse index to the dense array .*/
    chunk->sparse[OFFSET(index)] = dense;
    dense_array[dense] = index;
}

static
uint64_t inc_gen(
    uint64_t index)
{
    /* When an index is deleted, its generation is increased so that we can do
     * liveliness checking while recycling ids */
    return ECS_GENERATION_INC(index);
}

static
uint64_t inc_id(
    ecs_sparse_t *sparse)
{
    /* Generate a new id. The last issued id could be stored in an external
     * variable, such as is the case with the last issued entity id, which is
     * stored on the world. */
    return ++ (sparse->max_id[0]);
}

static
uint64_t get_id(
    const ecs_sparse_t *sparse)
{
    return sparse->max_id[0];
}

static
void set_id(
    ecs_sparse_t *sparse,
    uint64_t value)
{
    /* Sometimes the max id needs to be assigned directly, which typically 
     * happens when the API calls get_or_create for an id that hasn't been 
     * issued before. */
    sparse->max_id[0] = value;
}

/* Pair dense id with new sparse id */
static
uint64_t create_id(
    ecs_sparse_t *sparse,
    int32_t dense)
{
    uint64_t index = inc_id(sparse);
    grow_dense(sparse);

    chunk_t *chunk = get_or_create_chunk(sparse, CHUNK(index));
    ecs_assert(chunk->sparse[OFFSET(index)] == 0, ECS_INTERNAL_ERROR, NULL);
    
    uint64_t *dense_array = ecs_vector_first(sparse->dense, uint64_t);
    assign_index(chunk, dense_array, index, dense);
    
    return index;
}

/* Create new id */
static
uint64_t new_index(
    ecs_sparse_t *sparse)
{
    ecs_vector_t *dense = sparse->dense;
    int32_t dense_count = ecs_vector_count(dense);
    int32_t count = sparse->count ++;

    ecs_assert(count <= dense_count, ECS_INTERNAL_ERROR, NULL);

    if (count < dense_count) {
        /* If there are unused elements in the dense array, return first */
        uint64_t *dense_array = ecs_vector_first(dense, uint64_t);
        return dense_array[count];
    } else {
        return create_id(sparse, count);
    }
}

/* Try obtaining a value from the sparse set, don't care about whether the
 * provided index matches the current generation count.  */
static
void* try_sparse_any(
    const ecs_sparse_t *sparse,
    uint64_t index)
{    
    strip_generation(&index);

    chunk_t *chunk = get_chunk(sparse, CHUNK(index));
    if (!chunk) {
        return NULL;
    }

    int32_t offset = OFFSET(index);
    int32_t dense = chunk->sparse[offset];
    bool in_use = dense && (dense < sparse->count);
    if (!in_use) {
        return NULL;
    }

    ecs_assert(dense == chunk->sparse[offset], ECS_INTERNAL_ERROR, NULL);
    return DATA(chunk->data, sparse->size, offset);
}

/* Try obtaining a value from the sparse set, make sure it's alive. */
static
void* try_sparse(
    const ecs_sparse_t *sparse,
    uint64_t index)
{
    chunk_t *chunk = get_chunk(sparse, CHUNK(index));
    if (!chunk) {
        return NULL;
    }

    int32_t offset = OFFSET(index);
    int32_t dense = chunk->sparse[offset];
    bool in_use = dense && (dense < sparse->count);
    if (!in_use) {
        return NULL;
    }

    uint64_t gen = strip_generation(&index);
    uint64_t *dense_array = ecs_vector_first(sparse->dense, uint64_t);
    uint64_t cur_gen = dense_array[dense] & ECS_GENERATION_MASK;

    if (cur_gen != gen) {
        return NULL;
    }

    ecs_assert(dense == chunk->sparse[offset], ECS_INTERNAL_ERROR, NULL);
    return DATA(chunk->data, sparse->size, offset);
}

/* Get value from sparse set when it is guaranteed that the value exists. This
 * function is used when values are obtained using a dense index */
static
void* get_sparse(
    const ecs_sparse_t *sparse,
    int32_t dense,
    uint64_t index)
{
    strip_generation(&index);
    chunk_t *chunk = get_chunk(sparse, CHUNK(index));
    int32_t offset = OFFSET(index);
    
    ecs_assert(chunk != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(dense == chunk->sparse[offset], ECS_INTERNAL_ERROR, NULL);
    (void)dense;

    return DATA(chunk->data, sparse->size, offset);
}

/* Swap dense elements. A swap occurs when an element is removed, or when a
 * removed element is recycled. */
static
void swap_dense(
    ecs_sparse_t * sparse,
    chunk_t * chunk_a,
    int32_t a,
    int32_t b)
{
    ecs_assert(a != b, ECS_INTERNAL_ERROR, NULL);
    uint64_t *dense_array = ecs_vector_first(sparse->dense, uint64_t);
    uint64_t index_a = dense_array[a];
    uint64_t index_b = dense_array[b];

    chunk_t *chunk_b = get_or_create_chunk(sparse, CHUNK(index_b));
    assign_index(chunk_a, dense_array, index_a, b);
    assign_index(chunk_b, dense_array, index_b, a);
}

void _flecs_sparse_init(
    ecs_sparse_t *result,
    ecs_size_t size)
{
    ecs_assert(result != NULL, ECS_OUT_OF_MEMORY, NULL);
    result->size = size;
    result->max_id_local = UINT64_MAX;
    result->max_id = &result->max_id_local;

    /* Consume first value in dense array as 0 is used in the sparse array to
     * indicate that a sparse element hasn't been paired yet. */
    uint64_t *first = ecs_vector_add(&result->dense, uint64_t);
    *first = 0;

    result->count = 1;
}

ecs_sparse_t* _flecs_sparse_new(
    ecs_size_t size)
{
    ecs_sparse_t *result = ecs_os_calloc_t(ecs_sparse_t);

    _flecs_sparse_init(result, size);

    return result;
}

void flecs_sparse_set_id_source(
    ecs_sparse_t * sparse,
    uint64_t * id_source)
{
    ecs_assert(sparse != NULL, ECS_INVALID_PARAMETER, NULL);
    sparse->max_id = id_source;
}

void flecs_sparse_clear(
    ecs_sparse_t *sparse)
{
    ecs_assert(sparse != NULL, ECS_INVALID_PARAMETER, NULL);

    ecs_vector_each(sparse->chunks, chunk_t, chunk, {
        chunk_free(chunk);
    });

    ecs_vector_free(sparse->chunks);
    ecs_vector_set_count(&sparse->dense, uint64_t, 1);

    sparse->chunks = NULL;   
    sparse->count = 1;
    sparse->max_id_local = 0;
}

void _flecs_sparse_fini(
    ecs_sparse_t *sparse)
{
    ecs_assert(sparse != NULL, ECS_INTERNAL_ERROR, NULL);
    flecs_sparse_clear(sparse);
    ecs_vector_free(sparse->dense);
}

void flecs_sparse_free(
    ecs_sparse_t *sparse)
{
    if (sparse) {
        _flecs_sparse_fini(sparse);
        ecs_os_free(sparse);
    }
}

uint64_t flecs_sparse_new_id(
    ecs_sparse_t *sparse)
{
    ecs_assert(sparse != NULL, ECS_INVALID_PARAMETER, NULL);
    return new_index(sparse);
}

const uint64_t* flecs_sparse_new_ids(
    ecs_sparse_t *sparse,
    int32_t new_count)
{
    ecs_assert(sparse != NULL, ECS_INVALID_PARAMETER, NULL);
    int32_t dense_count = ecs_vector_count(sparse->dense);
    int32_t count = sparse->count;
    int32_t remaining = dense_count - count;
    int32_t i, to_create = new_count - remaining;

    if (to_create > 0) {
        flecs_sparse_set_size(sparse, dense_count + to_create);
        uint64_t *dense_array = ecs_vector_first(sparse->dense, uint64_t);

        for (i = 0; i < to_create; i ++) {
            uint64_t index = create_id(sparse, count + i);
            dense_array[dense_count + i] = index;
        }
    }

    sparse->count += new_count;

    return ecs_vector_get(sparse->dense, uint64_t, count);
}

void* _flecs_sparse_add(
    ecs_sparse_t *sparse,
    ecs_size_t size)
{
    ecs_assert(sparse != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(!size || size == sparse->size, ECS_INVALID_PARAMETER, NULL);
    uint64_t index = new_index(sparse);
    chunk_t *chunk = get_chunk(sparse, CHUNK(index));
    ecs_assert(chunk != NULL, ECS_INTERNAL_ERROR, NULL);
    return DATA(chunk->data, size, OFFSET(index));
}

uint64_t flecs_sparse_last_id(
    const ecs_sparse_t *sparse)
{
    ecs_assert(sparse != NULL, ECS_INTERNAL_ERROR, NULL);
    uint64_t *dense_array = ecs_vector_first(sparse->dense, uint64_t);
    return dense_array[sparse->count - 1];
}

void* _flecs_sparse_ensure(
    ecs_sparse_t *sparse,
    ecs_size_t size,
    uint64_t index)
{
    ecs_assert(sparse != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(!size || size == sparse->size, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(ecs_vector_count(sparse->dense) > 0, ECS_INTERNAL_ERROR, NULL);
    (void)size;

    uint64_t gen = strip_generation(&index);
    chunk_t *chunk = get_or_create_chunk(sparse, CHUNK(index));
    int32_t offset = OFFSET(index);
    int32_t dense = chunk->sparse[offset];

    if (dense) {
        /* Check if element is alive. If element is not alive, update indices so
         * that the first unused dense element points to the sparse element. */
        int32_t count = sparse->count;
        if (dense == count) {
            /* If dense is the next unused element in the array, simply increase
             * the count to make it part of the alive set. */
            sparse->count ++;
        } else if (dense > count) {
            /* If dense is not alive, swap it with the first unused element. */
            swap_dense(sparse, chunk, dense, count);

            /* First unused element is now last used element */
            sparse->count ++;
        } else {
            /* Dense is already alive, nothing to be done */
        }

        /* Ensure provided generation matches current. Only allow mismatching
         * generations if the provided generation count is 0. This allows for
         * using the ensure function in combination with ids that have their
         * generation stripped. */
        ecs_vector_t *dense_vector = sparse->dense;
        uint64_t *dense_array = ecs_vector_first(dense_vector, uint64_t);    
        ecs_assert(!gen || dense_array[dense] == (index | gen), ECS_INTERNAL_ERROR, NULL);
        (void)dense_vector;
        (void)dense_array;
    } else {
        /* Element is not paired yet. Must add a new element to dense array */
        grow_dense(sparse);

        ecs_vector_t *dense_vector = sparse->dense;
        uint64_t *dense_array = ecs_vector_first(dense_vector, uint64_t);    
        int32_t dense_count = ecs_vector_count(dense_vector) - 1;
        int32_t count = sparse->count ++;

        /* If index is larger than max id, update max id */
        if (index >= get_id(sparse)) {
            set_id(sparse, index);
        }

        if (count < dense_count) {
            /* If there are unused elements in the list, move the first unused
             * element to the end of the list */
            uint64_t unused = dense_array[count];
            chunk_t *unused_chunk = get_or_create_chunk(sparse, CHUNK(unused));
            assign_index(unused_chunk, dense_array, unused, dense_count);
        }

        assign_index(chunk, dense_array, index, count);
        dense_array[count] |= gen;
    }

    return DATA(chunk->data, sparse->size, offset);
}

void* _flecs_sparse_set(
    ecs_sparse_t * sparse,
    ecs_size_t elem_size,
    uint64_t index,
    void* value)
{
    void *ptr = _flecs_sparse_ensure(sparse, elem_size, index);
    ecs_os_memcpy(ptr, value, elem_size);
    return ptr;
}

void* _flecs_sparse_remove_get(
    ecs_sparse_t *sparse,
    ecs_size_t size,
    uint64_t index)
{
    ecs_assert(sparse != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(!size || size == sparse->size, ECS_INVALID_PARAMETER, NULL);
    (void)size;

    chunk_t *chunk = get_or_create_chunk(sparse, CHUNK(index));
    uint64_t gen = strip_generation(&index);
    int32_t offset = OFFSET(index);
    int32_t dense = chunk->sparse[offset];

    if (dense) {
        uint64_t *dense_array = ecs_vector_first(sparse->dense, uint64_t);
        uint64_t cur_gen = dense_array[dense] & ECS_GENERATION_MASK;
        if (gen != cur_gen) {
            /* Generation doesn't match which means that the provided entity is
             * already not alive. */
            return NULL;
        }

        /* Increase generation */
        dense_array[dense] = index | inc_gen(cur_gen);
        
        int32_t count = sparse->count;
        
        if (dense == (count - 1)) {
            /* If dense is the last used element, simply decrease count */
            sparse->count --;
        } else if (dense < count) {
            /* If element is alive, move it to unused elements */
            swap_dense(sparse, chunk, dense, count - 1);
            sparse->count --;
        } else {
            /* Element is not alive, nothing to be done */
            return NULL;
        }

        /* Reset memory to zero on remove */
        return DATA(chunk->data, sparse->size, offset);
    } else {
        /* Element is not paired and thus not alive, nothing to be done */
        return NULL;
    }
}

void flecs_sparse_remove(
    ecs_sparse_t *sparse,
    uint64_t index)
{
    void *ptr = _flecs_sparse_remove_get(sparse, 0, index);
    if (ptr) {
        ecs_os_memset(ptr, 0, sparse->size);
    }
}

void flecs_sparse_set_generation(
    ecs_sparse_t *sparse,
    uint64_t index)
{
    ecs_assert(sparse != NULL, ECS_INVALID_PARAMETER, NULL);
    chunk_t *chunk = get_or_create_chunk(sparse, CHUNK(index));
    
    uint64_t index_w_gen = index;
    strip_generation(&index);
    int32_t offset = OFFSET(index);
    int32_t dense = chunk->sparse[offset];

    if (dense) {
        /* Increase generation */
        uint64_t *dense_array = ecs_vector_first(sparse->dense, uint64_t);
        dense_array[dense] = index_w_gen;
    } else {
        /* Element is not paired and thus not alive, nothing to be done */
    }
}

bool flecs_sparse_exists(
    const ecs_sparse_t *sparse,
    uint64_t index)
{
    ecs_assert(sparse != NULL, ECS_INVALID_PARAMETER, NULL);
    chunk_t *chunk = get_chunk(sparse, CHUNK(index));
    if (!chunk) {
        return false;
    }
    
    strip_generation(&index);
    int32_t offset = OFFSET(index);
    int32_t dense = chunk->sparse[offset];

    return dense != 0;
}

void* _flecs_sparse_get_dense(
    const ecs_sparse_t *sparse,
    ecs_size_t size,
    int32_t dense_index)
{
    ecs_assert(sparse != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(!size || size == sparse->size, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(dense_index < sparse->count, ECS_INVALID_PARAMETER, NULL);
    (void)size;

    dense_index ++;

    uint64_t *dense_array = ecs_vector_first(sparse->dense, uint64_t);
    return get_sparse(sparse, dense_index, dense_array[dense_index]);
}

bool flecs_sparse_is_alive(
    const ecs_sparse_t *sparse,
    uint64_t index)
{
    return try_sparse(sparse, index) != NULL;
}

uint64_t flecs_sparse_get_alive(
    const ecs_sparse_t *sparse,
    uint64_t index)
{
    chunk_t *chunk = get_chunk(sparse, CHUNK(index));
    if (!chunk) {
        return 0;
    }

    int32_t offset = OFFSET(index);
    int32_t dense = chunk->sparse[offset];
    uint64_t *dense_array = ecs_vector_first(sparse->dense, uint64_t);

    /* If dense is 0 (tombstone) this will return 0 */
    return dense_array[dense];
}

void* _flecs_sparse_get(
    const ecs_sparse_t *sparse,
    ecs_size_t size,
    uint64_t index)
{
    ecs_assert(sparse != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(sparse != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(!size || size == sparse->size, ECS_INVALID_PARAMETER, NULL);
    (void)size;
    return try_sparse(sparse, index);
}

void* _flecs_sparse_get_any(
    const ecs_sparse_t *sparse,
    ecs_size_t size,
    uint64_t index)
{
    ecs_assert(sparse != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(sparse != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(!size || size == sparse->size, ECS_INVALID_PARAMETER, NULL);
    (void)size;
    return try_sparse_any(sparse, index);
}

int32_t flecs_sparse_count(
    const ecs_sparse_t *sparse)
{
    if (!sparse) {
        return 0;
    }

    return sparse->count - 1;
}

int32_t flecs_sparse_not_alive_count(
    const ecs_sparse_t *sparse)
{
    if (!sparse) {
        return 0;
    }

    return ecs_vector_count(sparse->dense) - sparse->count;
}

int32_t flecs_sparse_size(
    const ecs_sparse_t *sparse)
{
    if (!sparse) {
        return 0;
    }
        
    return ecs_vector_count(sparse->dense) - 1;
}

const uint64_t* flecs_sparse_ids(
    const ecs_sparse_t *sparse)
{
    ecs_assert(sparse != NULL, ECS_INVALID_PARAMETER, NULL);
    return &(ecs_vector_first(sparse->dense, uint64_t)[1]);
}

void flecs_sparse_set_size(
    ecs_sparse_t *sparse,
    int32_t elem_count)
{
    ecs_assert(sparse != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_vector_set_size(&sparse->dense, uint64_t, elem_count);
}

static
void sparse_copy(
    ecs_sparse_t * dst,
    const ecs_sparse_t * src)
{
    flecs_sparse_set_size(dst, flecs_sparse_size(src));
    const uint64_t *indices = flecs_sparse_ids(src);
    
    ecs_size_t size = src->size;
    int32_t i, count = src->count;

    for (i = 0; i < count - 1; i ++) {
        uint64_t index = indices[i];
        void *src_ptr = _flecs_sparse_get(src, size, index);
        void *dst_ptr = _flecs_sparse_ensure(dst, size, index);
        flecs_sparse_set_generation(dst, index);
        ecs_os_memcpy(dst_ptr, src_ptr, size);
    }

    set_id(dst, get_id(src));

    ecs_assert(src->count == dst->count, ECS_INTERNAL_ERROR, NULL);
}

ecs_sparse_t* flecs_sparse_copy(
    const ecs_sparse_t *src)
{
    if (!src) {
        return NULL;
    }

    ecs_sparse_t *dst = _flecs_sparse_new(src->size);
    sparse_copy(dst, src);

    return dst;
}

void flecs_sparse_restore(
    ecs_sparse_t * dst,
    const ecs_sparse_t * src)
{
    ecs_assert(dst != NULL, ECS_INVALID_PARAMETER, NULL);
    dst->count = 1;
    if (src) {
        sparse_copy(dst, src);
    }
}

void flecs_sparse_memory(
    ecs_sparse_t *sparse,
    int32_t *allocd,
    int32_t *used)
{
    (void)sparse;
    (void)allocd;
    (void)used;
}

ecs_sparse_t* _ecs_sparse_new(
    ecs_size_t elem_size)
{
    return _flecs_sparse_new(elem_size);
}

void* _ecs_sparse_add(
    ecs_sparse_t *sparse,
    ecs_size_t elem_size)
{
    return _flecs_sparse_add(sparse, elem_size);
}

uint64_t ecs_sparse_last_id(
    const ecs_sparse_t *sparse)
{
    return flecs_sparse_last_id(sparse);
}

int32_t ecs_sparse_count(
    const ecs_sparse_t *sparse)
{
    return flecs_sparse_count(sparse);
}

void* _ecs_sparse_get_dense(
    const ecs_sparse_t *sparse,
    ecs_size_t elem_size,
    int32_t index)
{
    return _flecs_sparse_get_dense(sparse, elem_size, index);
}

void* _ecs_sparse_get(
    const ecs_sparse_t *sparse,
    ecs_size_t elem_size,
    uint64_t id)
{
    return _flecs_sparse_get(sparse, elem_size, id);
}

ecs_sparse_iter_t _flecs_sparse_iter(
    ecs_sparse_t *sparse,
    ecs_size_t elem_size)
{
    ecs_assert(sparse != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(elem_size == sparse->size, ECS_INVALID_PARAMETER, NULL);
    ecs_sparse_iter_t result;
    result.sparse = sparse;
    result.ids = flecs_sparse_ids(sparse);
    result.size = elem_size;
    result.i = 0;
    result.count = sparse->count - 1;
    return result;
}


#ifdef FLECS_SANITIZE
static 
void verify_nodes(
    ecs_switch_header_t *hdr,
    ecs_switch_node_t *nodes)
{
    if (!hdr) {
        return;
    }

    int32_t prev = -1, elem = hdr->element, count = 0;
    while (elem != -1) {
        ecs_assert(prev == nodes[elem].prev, ECS_INTERNAL_ERROR, NULL);
        prev = elem;
        elem = nodes[elem].next;
        count ++;
    }

    ecs_assert(count == hdr->count, ECS_INTERNAL_ERROR, NULL);
}
#else
#define verify_nodes(hdr, nodes)
#endif

static
ecs_switch_header_t *get_header(
    const ecs_switch_t *sw,
    uint64_t value)
{
    if (value == 0) {
        return NULL;
    }

    return ecs_map_get(&sw->headers, ecs_switch_header_t, value);
}

static
ecs_switch_header_t *ensure_header(
    ecs_switch_t *sw,
    uint64_t value)
{
    if (value == 0) {
        return NULL;
    }

    ecs_switch_header_t *node = get_header(sw, value);
    if (!node) {
        node = ecs_map_ensure(&sw->headers, ecs_switch_header_t, value);
        node->element = -1;
    }

    return node;
}

static
void remove_node(
    ecs_switch_header_t *hdr,
    ecs_switch_node_t *nodes,
    ecs_switch_node_t *node,
    int32_t element)
{
    ecs_assert(&nodes[element] == node, ECS_INTERNAL_ERROR, NULL);

    /* Update previous node/header */
    if (hdr->element == element) {
        ecs_assert(node->prev == -1, ECS_INVALID_PARAMETER, NULL);
        /* If this is the first node, update the header */
        hdr->element = node->next;
    } else {
        /* If this is not the first node, update the previous node to the 
         * removed node's next ptr */
        ecs_assert(node->prev != -1, ECS_INVALID_PARAMETER, NULL);
        ecs_switch_node_t *prev_node = &nodes[node->prev];
        prev_node->next = node->next;
    }

    /* Update next node */
    int32_t next = node->next;
    if (next != -1) {
        ecs_assert(next >= 0, ECS_INVALID_PARAMETER, NULL);
        /* If this is not the last node, update the next node to point to the
         * removed node's prev ptr */
        ecs_switch_node_t *next_node = &nodes[next];
        next_node->prev = node->prev;
    }

    /* Decrease count of current header */
    hdr->count --;
    ecs_assert(hdr->count >= 0, ECS_INTERNAL_ERROR, NULL);
}

void flecs_switch_init(
    ecs_switch_t *sw,
    int32_t elements)
{
    ecs_map_init(&sw->headers, ecs_switch_header_t, 1);
    sw->nodes = ecs_vector_new(ecs_switch_node_t, elements);
    sw->values = ecs_vector_new(uint64_t, elements);

    ecs_switch_node_t *nodes = ecs_vector_first(
        sw->nodes, ecs_switch_node_t);
    uint64_t *values = ecs_vector_first(
        sw->values, uint64_t);

    int i;
    for (i = 0; i < elements; i ++) {
        nodes[i].prev = -1;
        nodes[i].next = -1;
        values[i] = 0;
    }
}

ecs_switch_t* flecs_switch_new(
    int32_t elements)
{
    ecs_switch_t *result = ecs_os_malloc(ECS_SIZEOF(ecs_switch_t));

    flecs_switch_init(result, elements);

    return result;
}

void flecs_switch_clear(
    ecs_switch_t *sw)
{
    ecs_map_clear(&sw->headers);
    ecs_vector_free(sw->nodes);
    ecs_vector_free(sw->values);
    sw->nodes = NULL;
    sw->values = NULL;
}

void flecs_switch_fini(
    ecs_switch_t *sw)
{
    // ecs_os_free(sw->headers);
    ecs_map_fini(&sw->headers);
    ecs_vector_free(sw->nodes);
    ecs_vector_free(sw->values);
}

void flecs_switch_free(
    ecs_switch_t *sw)
{
    flecs_switch_fini(sw);
    ecs_os_free(sw);
}

void flecs_switch_add(
    ecs_switch_t *sw)
{
    ecs_switch_node_t *node = ecs_vector_add(&sw->nodes, ecs_switch_node_t);
    uint64_t *value = ecs_vector_add(&sw->values, uint64_t);
    node->prev = -1;
    node->next = -1;
    *value = 0;
}

void flecs_switch_set_count(
    ecs_switch_t *sw,
    int32_t count)
{
    int32_t old_count = ecs_vector_count(sw->nodes);
    if (old_count == count) {
        return;
    }

    ecs_vector_set_count(&sw->nodes, ecs_switch_node_t, count);
    ecs_vector_set_count(&sw->values, uint64_t, count);

    ecs_switch_node_t *nodes = ecs_vector_first(sw->nodes, ecs_switch_node_t);
    uint64_t *values = ecs_vector_first(sw->values, uint64_t);

    int32_t i;
    for (i = old_count; i < count; i ++) {
        ecs_switch_node_t *node = &nodes[i];
        node->prev = -1;
        node->next = -1;
        values[i] = 0;
    }
}

int32_t flecs_switch_count(
    ecs_switch_t *sw)
{
    ecs_assert(ecs_vector_count(sw->values) == ecs_vector_count(sw->nodes),
        ECS_INTERNAL_ERROR, NULL);
    return ecs_vector_count(sw->values);
}

void flecs_switch_ensure(
    ecs_switch_t *sw,
    int32_t count)
{
    int32_t old_count = ecs_vector_count(sw->nodes);
    if (old_count >= count) {
        return;
    }

    flecs_switch_set_count(sw, count);
}

void flecs_switch_addn(
    ecs_switch_t *sw,
    int32_t count)
{
    int32_t old_count = ecs_vector_count(sw->nodes);
    flecs_switch_set_count(sw, old_count + count);
}

void flecs_switch_set(
    ecs_switch_t *sw,
    int32_t element,
    uint64_t value)
{
    ecs_assert(sw != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(element < ecs_vector_count(sw->nodes), ECS_INVALID_PARAMETER, NULL);
    ecs_assert(element < ecs_vector_count(sw->values), ECS_INVALID_PARAMETER, NULL);
    ecs_assert(element >= 0, ECS_INVALID_PARAMETER, NULL);

    uint64_t *values = ecs_vector_first(sw->values, uint64_t);
    uint64_t cur_value = values[element];

    /* If the node is already assigned to the value, nothing to be done */
    if (cur_value == value) {
        return;
    }

    ecs_switch_node_t *nodes = ecs_vector_first(sw->nodes, ecs_switch_node_t);
    ecs_switch_node_t *node = &nodes[element];

    ecs_switch_header_t *dst_hdr = ensure_header(sw, value);
    ecs_switch_header_t *cur_hdr = get_header(sw, cur_value);

    verify_nodes(cur_hdr, nodes);
    verify_nodes(dst_hdr, nodes);

    /* If value is not 0, and dst_hdr is NULL, then this is not a valid value
     * for this switch */
    ecs_assert(dst_hdr != NULL || !value, ECS_INVALID_PARAMETER, NULL);

    if (cur_hdr) {
        remove_node(cur_hdr, nodes, node, element);
    }

    /* Now update the node itself by adding it as the first node of dst */
    node->prev = -1;
    values[element] = value;

    if (dst_hdr) {
        node->next = dst_hdr->element;

        /* Also update the dst header */
        int32_t first = dst_hdr->element;
        if (first != -1) {
            ecs_assert(first >= 0, ECS_INTERNAL_ERROR, NULL);
            ecs_switch_node_t *first_node = &nodes[first];
            first_node->prev = element;
        }

        dst_hdr->element = element;
        dst_hdr->count ++;        
    }
}

void flecs_switch_remove(
    ecs_switch_t *sw,
    int32_t element)
{
    ecs_assert(sw != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(element < ecs_vector_count(sw->nodes), ECS_INVALID_PARAMETER, NULL);
    ecs_assert(element >= 0, ECS_INVALID_PARAMETER, NULL);

    uint64_t *values = ecs_vector_first(sw->values, uint64_t);
    uint64_t value = values[element];
    ecs_switch_node_t *nodes = ecs_vector_first(sw->nodes, ecs_switch_node_t);
    ecs_switch_node_t *node = &nodes[element];

    /* If node is currently assigned to a case, remove it from the list */
    if (value != 0) {
        ecs_switch_header_t *hdr = get_header(sw, value);
        ecs_assert(hdr != NULL, ECS_INTERNAL_ERROR, NULL);

        verify_nodes(hdr, nodes);
        remove_node(hdr, nodes, node, element);
    }

    int32_t last_elem = ecs_vector_count(sw->nodes) - 1;
    if (last_elem != element) {
        ecs_switch_node_t *last = ecs_vector_last(sw->nodes, ecs_switch_node_t);
        int32_t next = last->next, prev = last->prev;
        if (next != -1) {
            ecs_switch_node_t *n = &nodes[next];
            n->prev = element;
        }

        if (prev != -1) {
            ecs_switch_node_t *n = &nodes[prev];
            n->next = element;
        } else {
            ecs_switch_header_t *hdr = get_header(sw, values[last_elem]);
            if (hdr && hdr->element != -1) {
                ecs_assert(hdr->element == last_elem, 
                    ECS_INTERNAL_ERROR, NULL);
                hdr->element = element;
            }
        }
    }

    /* Remove element from arrays */
    ecs_vector_remove(sw->nodes, ecs_switch_node_t, element);
    ecs_vector_remove(sw->values, uint64_t, element);
}

uint64_t flecs_switch_get(
    const ecs_switch_t *sw,
    int32_t element)
{
    ecs_assert(sw != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(element < ecs_vector_count(sw->nodes), ECS_INVALID_PARAMETER, NULL);
    ecs_assert(element < ecs_vector_count(sw->values), ECS_INVALID_PARAMETER, NULL);
    ecs_assert(element >= 0, ECS_INVALID_PARAMETER, NULL);

    uint64_t *values = ecs_vector_first(sw->values, uint64_t);
    return values[element];
}

ecs_vector_t* flecs_switch_values(
    const ecs_switch_t *sw)
{
    return sw->values;
}

int32_t flecs_switch_case_count(
    const ecs_switch_t *sw,
    uint64_t value)
{
    ecs_switch_header_t *hdr = get_header(sw, value);
    if (!hdr) {
        return 0;
    }

    return hdr->count;
}

void flecs_switch_swap(
    ecs_switch_t *sw,
    int32_t elem_1,
    int32_t elem_2)
{
    uint64_t v1 = flecs_switch_get(sw, elem_1);
    uint64_t v2 = flecs_switch_get(sw, elem_2);

    flecs_switch_set(sw, elem_2, v1);
    flecs_switch_set(sw, elem_1, v2);
}

int32_t flecs_switch_first(
    const ecs_switch_t *sw,
    uint64_t value)
{
    ecs_assert(sw != NULL, ECS_INVALID_PARAMETER, NULL);
    
    ecs_switch_header_t *hdr = get_header(sw, value);
    if (!hdr) {
        return -1;
    }

    return hdr->element;
}

int32_t flecs_switch_next(
    const ecs_switch_t *sw,
    int32_t element)
{
    ecs_assert(sw != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(element < ecs_vector_count(sw->nodes), ECS_INVALID_PARAMETER, NULL);
    ecs_assert(element >= 0, ECS_INVALID_PARAMETER, NULL);

    ecs_switch_node_t *nodes = ecs_vector_first(
        sw->nodes, ecs_switch_node_t);

    return nodes[element].next;
}


static
uint64_t name_index_hash(
    const void *ptr)
{
    const ecs_hashed_string_t *str = ptr;
    ecs_assert(str->hash != 0, ECS_INTERNAL_ERROR, NULL);
    return str->hash;
}

static
int name_index_compare(
    const void *ptr1, 
    const void *ptr2)
{
    const ecs_hashed_string_t *str1 = ptr1;
    const ecs_hashed_string_t *str2 = ptr2;
    ecs_size_t len1 = str1->length;
    ecs_size_t len2 = str2->length;
    if (len1 != len2) {
        return (len1 > len2) - (len1 < len2);
    }

    return ecs_os_memcmp(str1->value, str2->value, len1);
}

void flecs_name_index_init(
    ecs_hashmap_t *hm) 
{
    _flecs_hashmap_init(hm, 
        ECS_SIZEOF(ecs_hashed_string_t), ECS_SIZEOF(uint64_t), 
        name_index_hash, 
        name_index_compare);
}

ecs_hashmap_t* flecs_name_index_new(void) 
{
    ecs_hashmap_t *result = ecs_os_calloc_t(ecs_hashmap_t);
    flecs_name_index_init(result);
    return result;
}

void flecs_name_index_fini(
    ecs_hashmap_t *map)
{
    flecs_hashmap_fini(map);
}

void flecs_name_index_free(
    ecs_hashmap_t *map)
{
    if (map) {
        flecs_name_index_fini(map);
        ecs_os_free(map);
    }
}

ecs_hashed_string_t flecs_get_hashed_string(
    const char *name,
    ecs_size_t length,
    uint64_t hash)
{
    if (!length) {
        length = ecs_os_strlen(name);
    } else {
        ecs_assert(length == ecs_os_strlen(name), ECS_INTERNAL_ERROR, NULL);
    }

    if (!hash) {
        hash = flecs_hash(name, length);
    } else {
        ecs_assert(hash == flecs_hash(name, length), ECS_INTERNAL_ERROR, NULL);
    }

    return (ecs_hashed_string_t) {
        .value = (char*)name,
        .length = length,
        .hash = hash
    };
}

const uint64_t* flecs_name_index_find_ptr(
    const ecs_hashmap_t *map,
    const char *name,
    ecs_size_t length,
    uint64_t hash)
{
    ecs_hashed_string_t hs = flecs_get_hashed_string(name, length, hash);

    ecs_hm_bucket_t *b = flecs_hashmap_get_bucket(map, hs.hash);
    if (!b) {
        return NULL;
    }

    ecs_hashed_string_t *keys = ecs_vector_first(b->keys, ecs_hashed_string_t);
    int32_t i, count = ecs_vector_count(b->keys);

    for (i = 0; i < count; i ++) {
        ecs_hashed_string_t *key = &keys[i];
        ecs_assert(key->hash == hs.hash, ECS_INTERNAL_ERROR, NULL);

        if (hs.length != key->length) {
            continue;
        }

        if (!ecs_os_strcmp(name, key->value)) {
            uint64_t *e = ecs_vector_get(b->values, uint64_t, i);
            ecs_assert(e != NULL, ECS_INTERNAL_ERROR, NULL);
            return e;
        }
    }

    return NULL;
}

uint64_t flecs_name_index_find(
    const ecs_hashmap_t *map,
    const char *name,
    ecs_size_t length,
    uint64_t hash)
{
    const uint64_t *id = flecs_name_index_find_ptr(map, name, length, hash);
    if (id) {
        return id[0];
    }
    return 0;
}

void flecs_name_index_remove(
    ecs_hashmap_t *map,
    uint64_t e,
    uint64_t hash)
{
    ecs_hm_bucket_t *b = flecs_hashmap_get_bucket(map, hash);
    if (!b) {
        return;
    }

    uint64_t *ids = ecs_vector_first(b->values, uint64_t);
    int32_t i, count = ecs_vector_count(b->values);

    for (i = 0; i < count; i ++) {
        if (ids[i] == e) {
            flecs_hm_bucket_remove(map, b, hash, i);
            break;
        }
    }
}

void flecs_name_index_update_name(
    ecs_hashmap_t *map,
    uint64_t e,
    uint64_t hash,
    const char *name)
{
    ecs_hm_bucket_t *b = flecs_hashmap_get_bucket(map, hash);
    if (!b) {
        return;
    }

    uint64_t *ids = ecs_vector_first(b->values, uint64_t);
    int32_t i, count = ecs_vector_count(b->values);

    for (i = 0; i < count; i ++) {
        if (ids[i] == e) {
            ecs_hashed_string_t *key = ecs_vector_get(
                b->keys, ecs_hashed_string_t, i);
            key->value = (char*)name;
            ecs_assert(ecs_os_strlen(name) == key->length,
                ECS_INTERNAL_ERROR, NULL);
            ecs_assert(flecs_hash(name, key->length) == key->hash,
                ECS_INTERNAL_ERROR, NULL);
            return;
        }
    }

    /* Record must already have been in the index */
    ecs_abort(ECS_INTERNAL_ERROR, NULL);
}

void flecs_name_index_ensure(
    ecs_hashmap_t *map,
    uint64_t id,
    const char *name,
    ecs_size_t length,
    uint64_t hash)
{
    ecs_check(name != NULL, ECS_INVALID_PARAMETER, NULL);

    ecs_hashed_string_t key = flecs_get_hashed_string(name, length, hash);
    
    uint64_t existing = flecs_name_index_find(
        map, name, key.length, key.hash);
    if (existing) {
        if (existing != id) {
            ecs_abort(ECS_ALREADY_DEFINED, 
                "conflicting id registered with name '%s'", name);
        }
    }

    flecs_hashmap_result_t hmr = flecs_hashmap_ensure(
        map, &key, uint64_t);

    *((uint64_t*)hmr.value) = id;
error:
    return;
}


#ifdef ECS_TARGET_GNU
#pragma GCC diagnostic ignored "-Wimplicit-fallthrough"
#endif

/* See explanation below. The hashing function may read beyond the memory passed
 * into the hashing function, but only at word boundaries. This should be safe,
 * but trips up address sanitizers and valgrind.
 * This ensures clean valgrind logs in debug mode & the best perf in release */
#if !defined(FLECS_NDEBUG) || defined(ADDRESS_SANITIZER)
#ifndef VALGRIND
#define VALGRIND
#endif
#endif

/*
-------------------------------------------------------------------------------
lookup3.c, by Bob Jenkins, May 2006, Public Domain.
  http://burtleburtle.net/bob/c/lookup3.c
-------------------------------------------------------------------------------
*/

#ifdef ECS_TARGET_MSVC
//FIXME
#else
#include <sys/param.h>  /* attempt to define endianness */
#endif
#ifdef ECS_TARGET_LINUX
# include <endian.h>    /* attempt to define endianness */
#endif

/*
 * My best guess at if you are big-endian or little-endian.  This may
 * need adjustment.
 */
#if (defined(__BYTE_ORDER) && defined(__LITTLE_ENDIAN) && \
     __BYTE_ORDER == __LITTLE_ENDIAN) || \
    (defined(i386) || defined(__i386__) || defined(__i486__) || \
     defined(__i586__) || defined(__i686__) || defined(vax) || defined(MIPSEL))
# define HASH_LITTLE_ENDIAN 1
#elif (defined(__BYTE_ORDER) && defined(__BIG_ENDIAN) && \
       __BYTE_ORDER == __BIG_ENDIAN) || \
      (defined(sparc) || defined(POWERPC) || defined(mc68000) || defined(sel))
# define HASH_LITTLE_ENDIAN 0
#else
# define HASH_LITTLE_ENDIAN 0
#endif

#define rot(x,k) (((x)<<(k)) | ((x)>>(32-(k))))

/*
-------------------------------------------------------------------------------
mix -- mix 3 32-bit values reversibly.
This is reversible, so any information in (a,b,c) before mix() is
still in (a,b,c) after mix().
If four pairs of (a,b,c) inputs are run through mix(), or through
mix() in reverse, there are at least 32 bits of the output that
are sometimes the same for one pair and different for another pair.
This was tested for:
* pairs that differed by one bit, by two bits, in any combination
  of top bits of (a,b,c), or in any combination of bottom bits of
  (a,b,c).
* "differ" is defined as +, -, ^, or ~^.  For + and -, I transformed
  the output delta to a Gray code (a^(a>>1)) so a string of 1's (as
  is commonly produced by subtraction) look like a single 1-bit
  difference.
* the base values were pseudorandom, all zero but one bit set, or 
  all zero plus a counter that starts at zero.
Some k values for my "a-=c; a^=rot(c,k); c+=b;" arrangement that
satisfy this are
    4  6  8 16 19  4
    9 15  3 18 27 15
   14  9  3  7 17  3
Well, "9 15 3 18 27 15" didn't quite get 32 bits diffing
for "differ" defined as + with a one-bit base and a two-bit delta.  I
used http://burtleburtle.net/bob/hash/avalanche.html to choose 
the operations, constants, and arrangements of the variables.
This does not achieve avalanche.  There are input bits of (a,b,c)
that fail to affect some output bits of (a,b,c), especially of a.  The
most thoroughly mixed value is c, but it doesn't really even achieve
avalanche in c.
This allows some parallelism.  Read-after-writes are good at doubling
the number of bits affected, so the goal of mixing pulls in the opposite
direction as the goal of parallelism.  I did what I could.  Rotates
seem to cost as much as shifts on every machine I could lay my hands
on, and rotates are much kinder to the top and bottom bits, so I used
rotates.
-------------------------------------------------------------------------------
*/
#define mix(a,b,c) \
{ \
  a -= c;  a ^= rot(c, 4);  c += b; \
  b -= a;  b ^= rot(a, 6);  a += c; \
  c -= b;  c ^= rot(b, 8);  b += a; \
  a -= c;  a ^= rot(c,16);  c += b; \
  b -= a;  b ^= rot(a,19);  a += c; \
  c -= b;  c ^= rot(b, 4);  b += a; \
}

/*
-------------------------------------------------------------------------------
final -- final mixing of 3 32-bit values (a,b,c) into c
Pairs of (a,b,c) values differing in only a few bits will usually
produce values of c that look totally different.  This was tested for
* pairs that differed by one bit, by two bits, in any combination
  of top bits of (a,b,c), or in any combination of bottom bits of
  (a,b,c).
* "differ" is defined as +, -, ^, or ~^.  For + and -, I transformed
  the output delta to a Gray code (a^(a>>1)) so a string of 1's (as
  is commonly produced by subtraction) look like a single 1-bit
  difference.
* the base values were pseudorandom, all zero but one bit set, or 
  all zero plus a counter that starts at zero.
These constants passed:
 14 11 25 16 4 14 24
 12 14 25 16 4 14 24
and these came close:
  4  8 15 26 3 22 24
 10  8 15 26 3 22 24
 11  8 15 26 3 22 24
-------------------------------------------------------------------------------
*/
#define final(a,b,c) \
{ \
  c ^= b; c -= rot(b,14); \
  a ^= c; a -= rot(c,11); \
  b ^= a; b -= rot(a,25); \
  c ^= b; c -= rot(b,16); \
  a ^= c; a -= rot(c,4);  \
  b ^= a; b -= rot(a,14); \
  c ^= b; c -= rot(b,24); \
}


/*
 * hashlittle2: return 2 32-bit hash values
 *
 * This is identical to hashlittle(), except it returns two 32-bit hash
 * values instead of just one.  This is good enough for hash table
 * lookup with 2^^64 buckets, or if you want a second hash if you're not
 * happy with the first, or if you want a probably-unique 64-bit ID for
 * the key.  *pc is better mixed than *pb, so use *pc first.  If you want
 * a 64-bit value do something like "*pc + (((uint64_t)*pb)<<32)".
 */
static
void hashlittle2( 
  const void *key,       /* the key to hash */
  size_t      length,    /* length of the key */
  uint32_t   *pc,        /* IN: primary initval, OUT: primary hash */
  uint32_t   *pb)        /* IN: secondary initval, OUT: secondary hash */
{
  uint32_t a,b,c;                                          /* internal state */
  union { const void *ptr; size_t i; } u;     /* needed for Mac Powerbook G4 */

  /* Set up the internal state */
  a = b = c = 0xdeadbeef + ((uint32_t)length) + *pc;
  c += *pb;

  u.ptr = key;
  if (HASH_LITTLE_ENDIAN && ((u.i & 0x3) == 0)) {
    const uint32_t *k = (const uint32_t *)key;         /* read 32-bit chunks */
    const uint8_t  *k8;
    (void)k8;

    /*------ all but last block: aligned reads and affect 32 bits of (a,b,c) */
    while (length > 12)
    {
      a += k[0];
      b += k[1];
      c += k[2];
      mix(a,b,c);
      length -= 12;
      k += 3;
    }

    /*----------------------------- handle the last (probably partial) block */
    /* 
     * "k[2]&0xffffff" actually reads beyond the end of the string, but
     * then masks off the part it's not allowed to read.  Because the
     * string is aligned, the masked-off tail is in the same word as the
     * rest of the string.  Every machine with memory protection I've seen
     * does it on word boundaries, so is OK with this.  But VALGRIND will
     * still catch it and complain.  The masking trick does make the hash
     * noticably faster for short strings (like English words).
     */
#ifndef VALGRIND

    switch(length)
    {
    case 12: c+=k[2]; b+=k[1]; a+=k[0]; break;
    case 11: c+=k[2]&0xffffff; b+=k[1]; a+=k[0]; break;
    case 10: c+=k[2]&0xffff; b+=k[1]; a+=k[0]; break;
    case 9 : c+=k[2]&0xff; b+=k[1]; a+=k[0]; break;
    case 8 : b+=k[1]; a+=k[0]; break;
    case 7 : b+=k[1]&0xffffff; a+=k[0]; break;
    case 6 : b+=k[1]&0xffff; a+=k[0]; break;
    case 5 : b+=k[1]&0xff; a+=k[0]; break;
    case 4 : a+=k[0]; break;
    case 3 : a+=k[0]&0xffffff; break;
    case 2 : a+=k[0]&0xffff; break;
    case 1 : a+=k[0]&0xff; break;
    case 0 : *pc=c; *pb=b; return;  /* zero length strings require no mixing */
    }

#else /* make valgrind happy */

    k8 = (const uint8_t *)k;
    switch(length)
    {
    case 12: c+=k[2]; b+=k[1]; a+=k[0]; break;
    case 11: c+=((uint32_t)k8[10])<<16;  /* fall through */
    case 10: c+=((uint32_t)k8[9])<<8;    /* fall through */
    case 9 : c+=k8[8];                   /* fall through */
    case 8 : b+=k[1]; a+=k[0]; break;
    case 7 : b+=((uint32_t)k8[6])<<16;   /* fall through */
    case 6 : b+=((uint32_t)k8[5])<<8;    /* fall through */
    case 5 : b+=k8[4];                   /* fall through */
    case 4 : a+=k[0]; break;
    case 3 : a+=((uint32_t)k8[2])<<16;   /* fall through */
    case 2 : a+=((uint32_t)k8[1])<<8;    /* fall through */
    case 1 : a+=k8[0]; break;
    case 0 : *pc=c; *pb=b; return;  /* zero length strings require no mixing */
    }

#endif /* !valgrind */

  } else if (HASH_LITTLE_ENDIAN && ((u.i & 0x1) == 0)) {
    const uint16_t *k = (const uint16_t *)key;         /* read 16-bit chunks */
    const uint8_t  *k8;

    /*--------------- all but last block: aligned reads and different mixing */
    while (length > 12)
    {
      a += k[0] + (((uint32_t)k[1])<<16);
      b += k[2] + (((uint32_t)k[3])<<16);
      c += k[4] + (((uint32_t)k[5])<<16);
      mix(a,b,c);
      length -= 12;
      k += 6;
    }

    /*----------------------------- handle the last (probably partial) block */
    k8 = (const uint8_t *)k;
    switch(length)
    {
    case 12: c+=k[4]+(((uint32_t)k[5])<<16);
             b+=k[2]+(((uint32_t)k[3])<<16);
             a+=k[0]+(((uint32_t)k[1])<<16);
             break;
    case 11: c+=((uint32_t)k8[10])<<16;     /* fall through */
    case 10: c+=k[4];
             b+=k[2]+(((uint32_t)k[3])<<16);
             a+=k[0]+(((uint32_t)k[1])<<16);
             break;
    case 9 : c+=k8[8];                      /* fall through */
    case 8 : b+=k[2]+(((uint32_t)k[3])<<16);
             a+=k[0]+(((uint32_t)k[1])<<16);
             break;
    case 7 : b+=((uint32_t)k8[6])<<16;      /* fall through */
    case 6 : b+=k[2];
             a+=k[0]+(((uint32_t)k[1])<<16);
             break;
    case 5 : b+=k8[4];                      /* fall through */
    case 4 : a+=k[0]+(((uint32_t)k[1])<<16);
             break;
    case 3 : a+=((uint32_t)k8[2])<<16;      /* fall through */
    case 2 : a+=k[0];
             break;
    case 1 : a+=k8[0];
             break;
    case 0 : *pc=c; *pb=b; return;  /* zero length strings require no mixing */
    }

  } else {                        /* need to read the key one byte at a time */
    const uint8_t *k = (const uint8_t *)key;

    /*--------------- all but the last block: affect some 32 bits of (a,b,c) */
    while (length > 12)
    {
      a += k[0];
      a += ((uint32_t)k[1])<<8;
      a += ((uint32_t)k[2])<<16;
      a += ((uint32_t)k[3])<<24;
      b += k[4];
      b += ((uint32_t)k[5])<<8;
      b += ((uint32_t)k[6])<<16;
      b += ((uint32_t)k[7])<<24;
      c += k[8];
      c += ((uint32_t)k[9])<<8;
      c += ((uint32_t)k[10])<<16;
      c += ((uint32_t)k[11])<<24;
      mix(a,b,c);
      length -= 12;
      k += 12;
    }

    /*-------------------------------- last block: affect all 32 bits of (c) */
    switch(length)                   /* all the case statements fall through */
    {
    case 12: c+=((uint32_t)k[11])<<24;
    case 11: c+=((uint32_t)k[10])<<16;
    case 10: c+=((uint32_t)k[9])<<8;
    case 9 : c+=k[8];
    case 8 : b+=((uint32_t)k[7])<<24;
    case 7 : b+=((uint32_t)k[6])<<16;
    case 6 : b+=((uint32_t)k[5])<<8;
    case 5 : b+=k[4];
    case 4 : a+=((uint32_t)k[3])<<24;
    case 3 : a+=((uint32_t)k[2])<<16;
    case 2 : a+=((uint32_t)k[1])<<8;
    case 1 : a+=k[0];
             break;
    case 0 : *pc=c; *pb=b; return;  /* zero length strings require no mixing */
    }
  }

  final(a,b,c);
  *pc=c; *pb=b;
}

uint64_t flecs_hash(
    const void *data,
    ecs_size_t length)
{
    uint32_t h_1 = 0;
    uint32_t h_2 = 0;

    hashlittle2(
        data,
        flecs_ito(size_t, length),
        &h_1,
        &h_2);

    return h_1 | ((uint64_t)h_2 << 32);
}


void ecs_qsort(
    void *base, 
    ecs_size_t nitems, 
    ecs_size_t size, 
    int (*compar)(const void *, const void*))
{
    void *tmp = ecs_os_alloca(size); /* For swap */

    #define LESS(i, j) \
        compar(ECS_ELEM(base, size, i), ECS_ELEM(base, size, j)) < 0

    #define SWAP(i, j) \
        ecs_os_memcpy(tmp, ECS_ELEM(base, size, i), size),\
        ecs_os_memcpy(ECS_ELEM(base, size, i), ECS_ELEM(base, size, j), size),\
        ecs_os_memcpy(ECS_ELEM(base, size, j), tmp, size)

    QSORT(nitems, LESS, SWAP);
}



static
void ensure(
    ecs_bitset_t *bs,
    ecs_size_t size)
{
    if (!bs->size) {
        int32_t new_size = ((size - 1) / 64 + 1) * ECS_SIZEOF(uint64_t);
        bs->size = ((size - 1) / 64 + 1) * 64;
        bs->data = ecs_os_calloc(new_size);
    } else if (size > bs->size) {
        int32_t prev_size = ((bs->size - 1) / 64 + 1) * ECS_SIZEOF(uint64_t);
        bs->size = ((size - 1) / 64 + 1) * 64;
        int32_t new_size = ((size - 1) / 64 + 1) * ECS_SIZEOF(uint64_t);
        bs->data = ecs_os_realloc(bs->data, new_size);
        ecs_os_memset(ECS_OFFSET(bs->data, prev_size), 0, new_size - prev_size);
    }
}

void flecs_bitset_init(
    ecs_bitset_t* bs)
{
    bs->size = 0;
    bs->count = 0;
    bs->data = NULL;
}

void flecs_bitset_ensure(
    ecs_bitset_t *bs,
    int32_t count)
{
    if (count > bs->count) {
        bs->count = count;
        ensure(bs, count);
    }
}

void flecs_bitset_fini(
    ecs_bitset_t *bs)
{
    ecs_os_free(bs->data);
    bs->data = NULL;
    bs->count = 0;
}

void flecs_bitset_addn(
    ecs_bitset_t *bs,
    int32_t count)
{
    int32_t elem = bs->count += count;
    ensure(bs, elem);
}

void flecs_bitset_set(
    ecs_bitset_t *bs,
    int32_t elem,
    bool value)
{
    ecs_check(elem < bs->count, ECS_INVALID_PARAMETER, NULL);
    uint32_t hi = ((uint32_t)elem) >> 6;
    uint32_t lo = ((uint32_t)elem) & 0x3F;
    uint64_t v = bs->data[hi];
    bs->data[hi] = (v & ~((uint64_t)1 << lo)) | ((uint64_t)value << lo);
error:
    return;
}

bool flecs_bitset_get(
    const ecs_bitset_t *bs,
    int32_t elem)
{
    ecs_check(elem < bs->count, ECS_INVALID_PARAMETER, NULL);
    return !!(bs->data[elem >> 6] & ((uint64_t)1 << ((uint64_t)elem & 0x3F)));
error:
    return false;
}

int32_t flecs_bitset_count(
    const ecs_bitset_t *bs)
{
    return bs->count;
}

void flecs_bitset_remove(
    ecs_bitset_t *bs,
    int32_t elem)
{
    ecs_check(elem < bs->count, ECS_INVALID_PARAMETER, NULL);
    int32_t last = bs->count - 1;
    bool last_value = flecs_bitset_get(bs, last);
    flecs_bitset_set(bs, elem, last_value);
    bs->count --;
error:
    return;
}

void flecs_bitset_swap(
    ecs_bitset_t *bs,
    int32_t elem_a,
    int32_t elem_b)
{
    ecs_check(elem_a < bs->count, ECS_INVALID_PARAMETER, NULL);
    ecs_check(elem_b < bs->count, ECS_INVALID_PARAMETER, NULL);

    bool a = flecs_bitset_get(bs, elem_a);
    bool b = flecs_bitset_get(bs, elem_b);
    flecs_bitset_set(bs, elem_a, b);
    flecs_bitset_set(bs, elem_b, a);
error:
    return;
}

#include <stdio.h>
#include <math.h>

/**
 *  stm32tpl --  STM32 C++ Template Peripheral Library
 *  Visit https://github.com/antongus/stm32tpl for new versions
 *
 *  Copyright (c) 2011-2020 Anton B. Gusev aka AHTOXA
 */

#define MAX_PRECISION	(10)
#define EXP_THRESHOLD   (3)
#define INT64_MAX_F ((double)INT64_MAX)

static const double rounders[MAX_PRECISION + 1] =
{
	0.5,				// 0
	0.05,				// 1
	0.005,				// 2
	0.0005,				// 3
	0.00005,			// 4
	0.000005,			// 5
	0.0000005,			// 6
	0.00000005,			// 7
	0.000000005,		// 8
	0.0000000005,		// 9
	0.00000000005		// 10
};

static
char* strbuf_itoa(
    char *buf,
    int64_t v)
{
    char *ptr = buf;
    char * p1;
	char c;

	if (!v) {
		*ptr++ = '0';
    } else {
		char *p = ptr;
		while (v) {
			*p++ = (char)('0' + v % 10);
			v /= 10;
		}

		p1 = p;

		while (p > ptr) {
			c = *--p;
			*p = *ptr;
			*ptr++ = c;
		}
		ptr = p1;
	}
    return ptr;
}

static
int ecs_strbuf_ftoa(
    ecs_strbuf_t *out, 
    double f, 
    int precision,
    char nan_delim)
{
    char buf[64];
	char * ptr = buf;
	char c;
	int64_t intPart;
    int64_t exp = 0;

    if (isnan(f)) {
        if (nan_delim) {
            ecs_strbuf_appendch(out, nan_delim);
            ecs_strbuf_appendstr(out, "NaN");
            return ecs_strbuf_appendch(out, nan_delim);
        } else {
            return ecs_strbuf_appendstr(out, "NaN");
        }
    }
    if (isinf(f)) {
        if (nan_delim) {
            ecs_strbuf_appendch(out, nan_delim);
            ecs_strbuf_appendstr(out, "Inf");
            return ecs_strbuf_appendch(out, nan_delim);
        } else {
            return ecs_strbuf_appendstr(out, "Inf");
        }
    }

	if (precision > MAX_PRECISION) {
		precision = MAX_PRECISION;
    }

	if (f < 0) {
		f = -f;
		*ptr++ = '-';
	}

	if (precision < 0) {
		if (f < 1.0) precision = 6;
		else if (f < 10.0) precision = 5;
		else if (f < 100.0) precision = 4;
		else if (f < 1000.0) precision = 3;
		else if (f < 10000.0) precision = 2;
		else if (f < 100000.0) precision = 1;
		else precision = 0;
	}

	if (precision) {
		f += rounders[precision];
    }

    /* Make sure that number can be represented as 64bit int, increase exp */
    while (f > INT64_MAX_F) {
        f /= 1000 * 1000 * 1000;
        exp += 9;
    }

	intPart = (int64_t)f;
	f -= (double)intPart;

    ptr = strbuf_itoa(ptr, intPart);

	if (precision) {
		*ptr++ = '.';
		while (precision--) {
			f *= 10.0;
			c = (char)f;
			*ptr++ = (char)('0' + c);
			f -= c;
		}
	}
	*ptr = 0;

    /* Remove trailing 0s */
    while ((&ptr[-1] != buf) && (ptr[-1] == '0')) {
        ptr[-1] = '\0';
        ptr --;
    }
    if (ptr != buf && ptr[-1] == '.') {
        ptr[-1] = '\0';
        ptr --;
    }

    /* If 0s before . exceed threshold, convert to exponent to save space 
     * without losing precision. */
    char *cur = ptr;
    while ((&cur[-1] != buf) && (cur[-1] == '0')) {
        cur --;
    }

    if (exp || ((ptr - cur) > EXP_THRESHOLD)) {
        cur[0] = '\0';
        exp += (ptr - cur);
        ptr = cur;
    }

    if (exp) {
        char *p1 = &buf[1];
        if (nan_delim) {
            ecs_os_memmove(buf + 1, buf, 1 + (ptr - buf));
            buf[0] = nan_delim;
            p1 ++;
        }

        /* Make sure that exp starts after first character */
        c = p1[0];

        if (c) {
            p1[0] = '.';
            do {
                char t = (++p1)[0];
                p1[0] = c;
                c = t;
                exp ++;
            } while (c);
            ptr = p1 + 1;
        } else {
            ptr = p1;
        }


        ptr[0] = 'e';
        ptr = strbuf_itoa(ptr + 1, exp);

        if (nan_delim) {
            ptr[0] = nan_delim;
            ptr ++;
        }

        ptr[0] = '\0';
    }
    
    return ecs_strbuf_appendstrn(out, buf, (int32_t)(ptr - buf));
}

/* Add an extra element to the buffer */
static
void ecs_strbuf_grow(
    ecs_strbuf_t *b)
{
    /* Allocate new element */
    ecs_strbuf_element_embedded *e = ecs_os_malloc_t(ecs_strbuf_element_embedded);
    b->size += b->current->pos;
    b->current->next = (ecs_strbuf_element*)e;
    b->current = (ecs_strbuf_element*)e;
    b->elementCount ++;
    e->super.buffer_embedded = true;
    e->super.buf = e->buf;
    e->super.pos = 0;
    e->super.next = NULL;
}

/* Add an extra dynamic element */
static
void ecs_strbuf_grow_str(
    ecs_strbuf_t *b,
    char *str,
    char *alloc_str,
    int32_t size)
{
    /* Allocate new element */
    ecs_strbuf_element_str *e = ecs_os_malloc_t(ecs_strbuf_element_str);
    b->size += b->current->pos;
    b->current->next = (ecs_strbuf_element*)e;
    b->current = (ecs_strbuf_element*)e;
    b->elementCount ++;
    e->super.buffer_embedded = false;
    e->super.pos = size ? size : (int32_t)ecs_os_strlen(str);
    e->super.next = NULL;
    e->super.buf = str;
    e->alloc_str = alloc_str;
}

static
char* ecs_strbuf_ptr(
    ecs_strbuf_t *b)
{
    if (b->buf) {
        return &b->buf[b->current->pos];
    } else {
        return &b->current->buf[b->current->pos];
    }
}

/* Compute the amount of space left in the current element */
static
int32_t ecs_strbuf_memLeftInCurrentElement(
    ecs_strbuf_t *b)
{
    if (b->current->buffer_embedded) {
        return ECS_STRBUF_ELEMENT_SIZE - b->current->pos;
    } else {
        return 0;
    }
}

/* Compute the amount of space left */
static
int32_t ecs_strbuf_memLeft(
    ecs_strbuf_t *b)
{
    if (b->max) {
        return b->max - b->size - b->current->pos;
    } else {
        return INT_MAX;
    }
}

static
void ecs_strbuf_init(
    ecs_strbuf_t *b)
{
    /* Initialize buffer structure only once */
    if (!b->elementCount) {
        b->size = 0;
        b->firstElement.super.next = NULL;
        b->firstElement.super.pos = 0;
        b->firstElement.super.buffer_embedded = true;
        b->firstElement.super.buf = b->firstElement.buf;
        b->elementCount ++;
        b->current = (ecs_strbuf_element*)&b->firstElement;
    }
}

/* Append a format string to a buffer */
static
bool vappend(
    ecs_strbuf_t *b,
    const char* str,
    va_list args)
{
    bool result = true;
    va_list arg_cpy;

    if (!str) {
        return result;
    }

    ecs_strbuf_init(b);

    int32_t memLeftInElement = ecs_strbuf_memLeftInCurrentElement(b);
    int32_t memLeft = ecs_strbuf_memLeft(b);

    if (!memLeft) {
        return false;
    }

    /* Compute the memory required to add the string to the buffer. If user
     * provided buffer, use space left in buffer, otherwise use space left in
     * current element. */
    int32_t max_copy = b->buf ? memLeft : memLeftInElement;
    int32_t memRequired;

    va_copy(arg_cpy, args);
    memRequired = vsnprintf(
        ecs_strbuf_ptr(b), (size_t)(max_copy + 1), str, args);

    ecs_assert(memRequired != -1, ECS_INTERNAL_ERROR, NULL);

    if (memRequired <= memLeftInElement) {
        /* Element was large enough to fit string */
        b->current->pos += memRequired;
    } else if ((memRequired - memLeftInElement) < memLeft) {
        /* If string is a format string, a new buffer of size memRequired is
         * needed to re-evaluate the format string and only use the part that
         * wasn't already copied to the previous element */
        if (memRequired <= ECS_STRBUF_ELEMENT_SIZE) {
            /* Resulting string fits in standard-size buffer. Note that the
             * entire string needs to fit, not just the remainder, as the
             * format string cannot be partially evaluated */
            ecs_strbuf_grow(b);

            /* Copy entire string to new buffer */
            ecs_os_vsprintf(ecs_strbuf_ptr(b), str, arg_cpy);

            /* Ignore the part of the string that was copied into the
             * previous buffer. The string copied into the new buffer could
             * be memmoved so that only the remainder is left, but that is
             * most likely more expensive than just keeping the entire
             * string. */

            /* Update position in buffer */
            b->current->pos += memRequired;
        } else {
            /* Resulting string does not fit in standard-size buffer.
             * Allocate a new buffer that can hold the entire string. */
            char *dst = ecs_os_malloc(memRequired + 1);
            ecs_os_vsprintf(dst, str, arg_cpy);
            ecs_strbuf_grow_str(b, dst, dst, memRequired);
        }
    }

    va_end(arg_cpy);

    return ecs_strbuf_memLeft(b) > 0;
}

static
bool appendstr(
    ecs_strbuf_t *b,
    const char* str,
    int n)
{
    ecs_strbuf_init(b);

    int32_t memLeftInElement = ecs_strbuf_memLeftInCurrentElement(b);
    int32_t memLeft = ecs_strbuf_memLeft(b);
    if (memLeft <= 0) {
        return false;
    }

    /* Never write more than what the buffer can store */
    if (n > memLeft) {
        n = memLeft;
    }

    if (n <= memLeftInElement) {
        /* Element was large enough to fit string */
        ecs_os_strncpy(ecs_strbuf_ptr(b), str, n);
        b->current->pos += n;
    } else if ((n - memLeftInElement) < memLeft) {
        ecs_os_strncpy(ecs_strbuf_ptr(b), str, memLeftInElement);

        /* Element was not large enough, but buffer still has space */
        b->current->pos += memLeftInElement;
        n -= memLeftInElement;

        /* Current element was too small, copy remainder into new element */
        if (n < ECS_STRBUF_ELEMENT_SIZE) {
            /* A standard-size buffer is large enough for the new string */
            ecs_strbuf_grow(b);

            /* Copy the remainder to the new buffer */
            if (n) {
                /* If a max number of characters to write is set, only a
                 * subset of the string should be copied to the buffer */
                ecs_os_strncpy(
                    ecs_strbuf_ptr(b),
                    str + memLeftInElement,
                    (size_t)n);
            } else {
                ecs_os_strcpy(ecs_strbuf_ptr(b), str + memLeftInElement);
            }

            /* Update to number of characters copied to new buffer */
            b->current->pos += n;
        } else {
            /* String doesn't fit in a single element, strdup */
            char *remainder = ecs_os_strdup(str + memLeftInElement);
            ecs_strbuf_grow_str(b, remainder, remainder, n);
        }
    } else {
        /* Buffer max has been reached */
        return false;
    }

    return ecs_strbuf_memLeft(b) > 0;
}

static
bool appendch(
    ecs_strbuf_t *b,
    char ch)
{
    ecs_strbuf_init(b);

    int32_t memLeftInElement = ecs_strbuf_memLeftInCurrentElement(b);
    int32_t memLeft = ecs_strbuf_memLeft(b);
    if (memLeft <= 0) {
        return false;
    }

    if (memLeftInElement) {
        /* Element was large enough to fit string */
        ecs_strbuf_ptr(b)[0] = ch;
        b->current->pos ++;
    } else {
        ecs_strbuf_grow(b);
        ecs_strbuf_ptr(b)[0] = ch;
        b->current->pos ++;
    }

    return ecs_strbuf_memLeft(b) > 0;
}

bool ecs_strbuf_vappend(
    ecs_strbuf_t *b,
    const char* fmt,
    va_list args)
{
    ecs_assert(b != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(fmt != NULL, ECS_INVALID_PARAMETER, NULL);
    return vappend(b, fmt, args);
}

bool ecs_strbuf_append(
    ecs_strbuf_t *b,
    const char* fmt,
    ...)
{
    ecs_assert(b != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(fmt != NULL, ECS_INVALID_PARAMETER, NULL);

    va_list args;
    va_start(args, fmt);
    bool result = vappend(b, fmt, args);
    va_end(args);

    return result;
}

bool ecs_strbuf_appendstrn(
    ecs_strbuf_t *b,
    const char* str,
    int32_t len)
{
    ecs_assert(b != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(str != NULL, ECS_INVALID_PARAMETER, NULL);
    return appendstr(b, str, len);
}

bool ecs_strbuf_appendch(
    ecs_strbuf_t *b,
    char ch)
{
    ecs_assert(b != NULL, ECS_INVALID_PARAMETER, NULL); 
    return appendch(b, ch);
}

bool ecs_strbuf_appendflt(
    ecs_strbuf_t *b,
    double flt,
    char nan_delim)
{
    ecs_assert(b != NULL, ECS_INVALID_PARAMETER, NULL); 
    return ecs_strbuf_ftoa(b, flt, 10, nan_delim);
}

bool ecs_strbuf_appendstr_zerocpy(
    ecs_strbuf_t *b,
    char* str)
{
    ecs_assert(b != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(str != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_strbuf_init(b);
    ecs_strbuf_grow_str(b, str, str, 0);
    return true;
}

bool ecs_strbuf_appendstr_zerocpy_const(
    ecs_strbuf_t *b,
    const char* str)
{
    ecs_assert(b != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(str != NULL, ECS_INVALID_PARAMETER, NULL);
    /* Removes const modifier, but logic prevents changing / delete string */
    ecs_strbuf_init(b);
    ecs_strbuf_grow_str(b, (char*)str, NULL, 0);
    return true;
}

bool ecs_strbuf_appendstr(
    ecs_strbuf_t *b,
    const char* str)
{
    ecs_assert(b != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(str != NULL, ECS_INVALID_PARAMETER, NULL);
    return appendstr(b, str, ecs_os_strlen(str));
}

bool ecs_strbuf_mergebuff(
    ecs_strbuf_t *dst_buffer,
    ecs_strbuf_t *src_buffer)
{
    if (src_buffer->elementCount) {
        if (src_buffer->buf) {
            return ecs_strbuf_appendstr(dst_buffer, src_buffer->buf);
        } else {
            ecs_strbuf_element *e = (ecs_strbuf_element*)&src_buffer->firstElement;

            /* Copy first element as it is inlined in the src buffer */
            ecs_strbuf_appendstrn(dst_buffer, e->buf, e->pos);

            while ((e = e->next)) {
                dst_buffer->current->next = ecs_os_malloc(sizeof(ecs_strbuf_element));
                *dst_buffer->current->next = *e;
            }
        }

        *src_buffer = ECS_STRBUF_INIT;
    }

    return true;
}

char* ecs_strbuf_get(
    ecs_strbuf_t *b) 
{
    ecs_assert(b != NULL, ECS_INVALID_PARAMETER, NULL);

    char* result = NULL;
    if (b->elementCount) {
        if (b->buf) {
            b->buf[b->current->pos] = '\0';
            result = ecs_os_strdup(b->buf);
        } else {
            void *next = NULL;
            int32_t len = b->size + b->current->pos + 1;

            ecs_strbuf_element *e = (ecs_strbuf_element*)&b->firstElement;

            result = ecs_os_malloc(len);
            char* ptr = result;

            do {
                ecs_os_memcpy(ptr, e->buf, e->pos);
                ptr += e->pos;
                next = e->next;
                if (e != &b->firstElement.super) {
                    if (!e->buffer_embedded) {
                        ecs_os_free(((ecs_strbuf_element_str*)e)->alloc_str);
                    }
                    ecs_os_free(e);
                }
            } while ((e = next));

            result[len - 1] = '\0';
            b->length = len;
        }
    } else {
        result = NULL;
    }

    b->elementCount = 0;

    b->content = result;

    return result;
}

char *ecs_strbuf_get_small(
    ecs_strbuf_t *b)
{
    ecs_assert(b != NULL, ECS_INVALID_PARAMETER, NULL);

    int32_t written = ecs_strbuf_written(b);
    ecs_assert(written <= ECS_STRBUF_ELEMENT_SIZE, ECS_INVALID_OPERATION, NULL);
    char *buf = b->firstElement.buf;
    buf[written] = '\0';
    return buf;
}

void ecs_strbuf_reset(
    ecs_strbuf_t *b) 
{
    ecs_assert(b != NULL, ECS_INVALID_PARAMETER, NULL);

    if (b->elementCount && !b->buf) {
        void *next = NULL;
        ecs_strbuf_element *e = (ecs_strbuf_element*)&b->firstElement;
        do {
            next = e->next;
            if (e != (ecs_strbuf_element*)&b->firstElement) {
                ecs_os_free(e);
            }
        } while ((e = next));
    }

    *b = ECS_STRBUF_INIT;
}

void ecs_strbuf_list_push(
    ecs_strbuf_t *b,
    const char *list_open,
    const char *separator)
{
    ecs_assert(b != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(list_open != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(separator != NULL, ECS_INVALID_PARAMETER, NULL);

    b->list_sp ++;
    b->list_stack[b->list_sp].count = 0;
    b->list_stack[b->list_sp].separator = separator;

    if (list_open) {
        ecs_strbuf_appendstr(b, list_open);
    }
}

void ecs_strbuf_list_pop(
    ecs_strbuf_t *b,
    const char *list_close)
{
    ecs_assert(b != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(list_close != NULL, ECS_INVALID_PARAMETER, NULL);

    b->list_sp --;
    
    if (list_close) {
        ecs_strbuf_appendstr(b, list_close);
    }
}

void ecs_strbuf_list_next(
    ecs_strbuf_t *b)
{
    ecs_assert(b != NULL, ECS_INVALID_PARAMETER, NULL);

    int32_t list_sp = b->list_sp;
    if (b->list_stack[list_sp].count != 0) {
        ecs_strbuf_appendstr(b, b->list_stack[list_sp].separator);
    }
    b->list_stack[list_sp].count ++;
}

bool ecs_strbuf_list_append(
    ecs_strbuf_t *b,
    const char *fmt,
    ...)
{
    ecs_assert(b != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(fmt != NULL, ECS_INVALID_PARAMETER, NULL);

    ecs_strbuf_list_next(b);

    va_list args;
    va_start(args, fmt);
    bool result = vappend(b, fmt, args);
    va_end(args);

    return result;
}

bool ecs_strbuf_list_appendstr(
    ecs_strbuf_t *b,
    const char *str)
{
    ecs_assert(b != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(str != NULL, ECS_INVALID_PARAMETER, NULL);

    ecs_strbuf_list_next(b);
    return ecs_strbuf_appendstr(b, str);
}

int32_t ecs_strbuf_written(
    const ecs_strbuf_t *b)
{
    ecs_assert(b != NULL, ECS_INVALID_PARAMETER, NULL);
    if (b->current) {
        return b->size + b->current->pos;
    } else {
        return 0;
    }
}

#include <math.h>

/* The ratio used to determine whether the map should rehash. If
 * (element_count * LOAD_FACTOR) > bucket_count, bucket count is increased. */
#define LOAD_FACTOR (1.2f)
#define KEY_SIZE (ECS_SIZEOF(ecs_map_key_t))
#define GET_ELEM(array, elem_size, index) \
    ECS_OFFSET(array, (elem_size) * (index))

static
ecs_block_allocator_chunk_header_t *ecs_balloc_block(
    ecs_block_allocator_t *allocator)
{
    ecs_block_allocator_chunk_header_t *first_chunk = 
        ecs_os_malloc(allocator->block_size);
    ecs_block_allocator_block_t *block = 
        ecs_os_malloc_t(ecs_block_allocator_block_t);

    block->memory = first_chunk;
    if (!allocator->block_tail) {
        ecs_assert(!allocator->block_head, ECS_INTERNAL_ERROR, 0);
        block->next = NULL;
        allocator->block_head = block;
        allocator->block_tail = block;
    } else {
        block->next = NULL;
        allocator->block_tail->next = block;
        allocator->block_tail = block;
    }

    ecs_block_allocator_chunk_header_t *chunk = first_chunk;
    int32_t i, end;
    for (i = 0, end = allocator->chunks_per_block - 1; i < end; ++i) {
        chunk->next = ECS_OFFSET(chunk, allocator->chunk_size);
        chunk = chunk->next;
    }

    chunk->next = NULL;
    return first_chunk;
}

static 
void *ecs_balloc(
    ecs_block_allocator_t *allocator) 
{
    if (!allocator->head) {
        allocator->head = ecs_balloc_block(allocator);
    }

    void *result = allocator->head;
    allocator->head = allocator->head->next;
    return result;
}

static 
void ecs_bfree(
    ecs_block_allocator_t *allocator, 
    void *memory) 
{
    ecs_block_allocator_chunk_header_t *chunk = memory;
    chunk->next = allocator->head;
    allocator->head = chunk;
}

static
uint8_t ecs_log2(uint32_t v) {
    static const uint8_t log2table[32] = 
        {0, 9,  1,  10, 13, 21, 2,  29, 11, 14, 16, 18, 22, 25, 3, 30,
         8, 12, 20, 28, 15, 17, 24, 7,  19, 27, 23, 6,  26, 5,  4, 31};

    v |= v >> 1;
    v |= v >> 2;
    v |= v >> 4;
    v |= v >> 8;
    v |= v >> 16;
    return log2table[(uint32_t)(v * 0x07C4ACDDU) >> 27];
}

/* Get bucket count for number of elements */
static
int32_t get_bucket_count(
    int32_t element_count)
{
    return flecs_next_pow_of_2((int32_t)((float)element_count * LOAD_FACTOR));
}

/* Get bucket shift amount for a given bucket count */
static
uint8_t get_bucket_shift (
    int32_t bucket_count)
{
    return (uint8_t)(64u - ecs_log2((uint32_t)bucket_count));
}

/* Get bucket index for provided map key */
static
int32_t get_bucket_index(
    uint16_t bucket_shift,
    ecs_map_key_t key) 
{
    ecs_assert(bucket_shift != 0, ECS_INTERNAL_ERROR, NULL);
    return (int32_t)((11400714819323198485ull * key) >> bucket_shift);
}

/* Get bucket for key */
static
ecs_bucket_t* get_bucket(
    const ecs_map_t *map,
    ecs_map_key_t key)
{
    ecs_assert(map->bucket_shift == get_bucket_shift(map->bucket_count),
        ECS_INTERNAL_ERROR, NULL);
    int32_t bucket_id = get_bucket_index(map->bucket_shift, key);
    ecs_assert(bucket_id < map->bucket_count, ECS_INTERNAL_ERROR, NULL);
    return &map->buckets[bucket_id];
}

/* Ensure that map has at least new_count buckets */
static
void ensure_buckets(
    ecs_map_t *map,
    int32_t new_count)
{
    int32_t bucket_count = map->bucket_count;
    new_count = flecs_next_pow_of_2(new_count);
    if (new_count < 2) {
        new_count = 2;
    }

    if (new_count && new_count > bucket_count) {
        map->buckets = ecs_os_realloc_n(map->buckets, ecs_bucket_t, new_count);
        map->buckets_end = ECS_ELEM_T(map->buckets, ecs_bucket_t, new_count);

        map->bucket_count = new_count;
        map->bucket_shift = get_bucket_shift(new_count);
        ecs_os_memset_n(ECS_ELEM_T(map->buckets, ecs_bucket_t, bucket_count),
            0, ecs_bucket_t, (new_count - bucket_count));
    }
}

/* Free contents of bucket */
static
void clear_bucket(
    ecs_block_allocator_t *allocator,
    ecs_bucket_entry_t *bucket)
{
    while(bucket) {
      ecs_bucket_entry_t *next = bucket->next;
      ecs_bfree(allocator, bucket);
      bucket = next;
    }
}

/* Clear all buckets */
static
void clear_buckets(
    ecs_map_t *map)
{
    int32_t i, count = map->bucket_count;
    for (i = 0; i < count; i ++) {
        clear_bucket(&map->allocator, map->buckets[i].first);
    }
    ecs_os_free(map->buckets);
    map->buckets = NULL;
    map->bucket_count = 0;
}

/* Add element to bucket */
static
void* add_to_bucket(
    ecs_block_allocator_t *allocator,
    ecs_bucket_t *bucket,
    ecs_size_t elem_size,
    ecs_map_key_t key,
    const void *payload)
{
    ecs_bucket_entry_t *new_entry = ecs_balloc(allocator);
    new_entry->key = key;
    new_entry->next = bucket->first;
    bucket->first = new_entry;
    void *new_payload = ECS_OFFSET(&new_entry->key, ECS_SIZEOF(ecs_map_key_t));
    if (elem_size && payload) {
        ecs_os_memcpy(new_payload, payload, elem_size);
    }
    return new_payload;
}

/*  Remove element from bucket */
static
bool remove_from_bucket(
    ecs_block_allocator_t *allocator,
    ecs_bucket_t *bucket,
    ecs_map_key_t key)
{
    ecs_bucket_entry_t *entry;
    for (entry = bucket->first; entry; entry = entry->next) {
        if (entry->key == key) {
            ecs_bucket_entry_t **next_holder = &bucket->first;
            while(*next_holder != entry) {
                next_holder = &(*next_holder)->next;
            }
            *next_holder = entry->next;
            ecs_bfree(allocator, entry);
            return true;
        }
    }
    
    return false;
}

/* Get payload pointer for key from bucket */
static
void* get_from_bucket(
    ecs_bucket_t *bucket,
    ecs_map_key_t key)
{
    ecs_bucket_entry_t *entry;
    for (entry = bucket->first; entry; entry = entry->next) {
        if (entry->key == key) {
            return ECS_OFFSET(&entry->key, ECS_SIZEOF(ecs_map_key_t));
        }
    }
    
    return NULL;
}

/* Grow number of buckets */
static
void rehash(
    ecs_map_t *map,
    int32_t bucket_count)
{
    ecs_assert(bucket_count != 0, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(bucket_count > map->bucket_count, ECS_INTERNAL_ERROR, NULL);
    
    int32_t old_count = map->bucket_count;
    ecs_bucket_t *old_buckets = map->buckets;
    
    int32_t new_count = flecs_next_pow_of_2(bucket_count);
    map->bucket_count = new_count;
    map->bucket_shift = get_bucket_shift(new_count);
    map->buckets = ecs_os_calloc_n(ecs_bucket_t, new_count);
    map->buckets_end = ECS_ELEM_T(map->buckets, ecs_bucket_t, new_count);
    
    /* Remap old bucket entries to new buckets */
    int32_t index;
    for (index = 0; index < old_count; ++index) {
        ecs_bucket_entry_t* entry;
        for (entry = old_buckets[index].first; entry;) {
            ecs_bucket_entry_t* next = entry->next;
            int32_t bucket_index = get_bucket_index(
                map->bucket_shift, entry->key);
            ecs_bucket_t *bucket = &map->buckets[bucket_index];
            entry->next = bucket->first;
            bucket->first = entry;
            entry = next;
        }
    }
    
    ecs_os_free(old_buckets);
}

void _ecs_map_init(
    ecs_map_t *result,
    ecs_size_t elem_size,
    int32_t element_count)
{
    ecs_assert(elem_size < INT16_MAX, ECS_INVALID_PARAMETER, NULL);

    result->count = 0;
    result->elem_size = (int16_t)elem_size;
    
    int32_t entry_size = elem_size + ECS_SIZEOF(ecs_bucket_entry_t);
    int32_t balloc_min_chunk_size = ECS_MAX(entry_size, 
        ECS_SIZEOF(ecs_block_allocator_chunk_header_t));
    uint32_t alignment = 16;
    uint32_t alignment_mask = alignment - 1u;

    /* Align balloc_min_chunk_size up to alignment. */
    result->allocator.chunk_size = (int32_t)(((uint32_t)balloc_min_chunk_size + 
        alignment_mask) & ~alignment_mask);
    result->allocator.chunks_per_block = 
        ECS_MAX(4096 / result->allocator.chunk_size, 1);
    result->allocator.block_size = result->allocator.chunks_per_block * 
        result->allocator.chunk_size;
    result->allocator.head = NULL;
    result->allocator.block_head = NULL;
    result->allocator.block_tail = NULL;

    ensure_buckets(result, get_bucket_count(element_count));
}

ecs_map_t* _ecs_map_new(
    ecs_size_t elem_size,
    int32_t element_count)
{
    ecs_map_t *result = ecs_os_calloc_t(ecs_map_t);
    ecs_assert(result != NULL, ECS_OUT_OF_MEMORY, NULL);

    _ecs_map_init(result, elem_size, element_count);

    return result;
}

bool ecs_map_is_initialized(
    const ecs_map_t *result)
{
    return result != NULL && result->bucket_count != 0;
}

void ecs_map_fini(
    ecs_map_t *map)
{
    ecs_assert(map != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_block_allocator_block_t *block;
    for (block = map->allocator.block_head; block; ){
        ecs_block_allocator_block_t *next = block->next;
        ecs_os_free(block->memory);
        ecs_os_free(block);
        block = next;
    }
    map->allocator.block_head = NULL;
    ecs_os_free(map->buckets);
    map->buckets = NULL;
    map->buckets_end = NULL;
    map->bucket_count = 0;
    ecs_assert(!ecs_map_is_initialized(map), ECS_INTERNAL_ERROR, NULL);
}

void ecs_map_free(
    ecs_map_t *map)
{
    if (map) {
        ecs_map_fini(map);
        ecs_os_free(map);
    }
}

void* _ecs_map_get(
    const ecs_map_t *map,
    ecs_size_t elem_size,
    ecs_map_key_t key)
{
    (void)elem_size;

    if (!ecs_map_is_initialized(map)) {
        return NULL;
    }

    ecs_assert(elem_size == map->elem_size, ECS_INVALID_PARAMETER, NULL);

    ecs_bucket_t *bucket = get_bucket(map, key);

    return get_from_bucket(bucket, key);
}

void* _ecs_map_get_ptr(
    const ecs_map_t *map,
    ecs_map_key_t key)
{
    void* ptr_ptr = _ecs_map_get(map, ECS_SIZEOF(void*), key);

    if (ptr_ptr) {
        return *(void**)ptr_ptr;
    } else {
        return NULL;
    }
}

bool ecs_map_has(
    const ecs_map_t *map,
    ecs_map_key_t key)
{
    if (!ecs_map_is_initialized(map)) {
        return false;
    }

    ecs_bucket_t *bucket = get_bucket(map, key);

    return get_from_bucket(bucket, key) != NULL;
}

void* _ecs_map_ensure(
    ecs_map_t *map,
    ecs_size_t elem_size,
    ecs_map_key_t key)
{
    void *result = _ecs_map_get(map, elem_size, key);
    if (!result) {
        result = _ecs_map_set(map, elem_size, key, NULL);
        if (elem_size) {
            ecs_assert(result != NULL, ECS_INTERNAL_ERROR, NULL);
            ecs_os_memset(result, 0, elem_size);
        }
    }

    return result;
}

void* _ecs_map_set(
    ecs_map_t *map,
    ecs_size_t elem_size,
    ecs_map_key_t key,
    const void *payload)
{
    ecs_assert(map != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(elem_size == map->elem_size, ECS_INVALID_PARAMETER, NULL);

    ecs_bucket_t *bucket = get_bucket(map, key);

    void *elem = get_from_bucket(bucket, key);
    if (!elem) {
        void *added_data = add_to_bucket(
            &map->allocator, bucket, elem_size, key, payload);
        int32_t map_count = ++map->count;
        int32_t target_bucket_count = get_bucket_count(map_count);
        int32_t map_bucket_count = map->bucket_count;

        if (target_bucket_count > map_bucket_count) {
            rehash(map, target_bucket_count);
            bucket = get_bucket(map, key);
            added_data = get_from_bucket(bucket, key);
            ecs_assert(added_data != NULL, ECS_INVALID_PARAMETER, NULL);
        }
        return added_data;
    } else {
        if (payload) {
            ecs_os_memcpy(elem, payload, elem_size);
        }
        return elem;
    }
}

int32_t ecs_map_remove(
    ecs_map_t *map,
    ecs_map_key_t key)
{
    ecs_assert(map != NULL, ECS_INVALID_PARAMETER, NULL);

    ecs_bucket_t *bucket = get_bucket(map, key);
    if (remove_from_bucket(&map->allocator, bucket, key)) {
        return --map->count;
    }
    
    return map->count;
}

int32_t ecs_map_count(
    const ecs_map_t *map)
{
    return map ? map->count : 0;
}

int32_t ecs_map_bucket_count(
    const ecs_map_t *map)
{
    return map ? map->bucket_count : 0;
}

void ecs_map_clear(
    ecs_map_t *map)
{
    ecs_assert(map != NULL, ECS_INVALID_PARAMETER, NULL);
    clear_buckets(map);
    map->count = 0;
    ensure_buckets(map, 2);
}

ecs_map_iter_t ecs_map_iter(
    const ecs_map_t *map)
{
    return (ecs_map_iter_t){
        .map = map,
        .bucket = NULL,
        .entry = NULL
    };
}

void* _ecs_map_next(
    ecs_map_iter_t *iter,
    ecs_size_t elem_size,
    ecs_map_key_t *key_out)
{
    (void)elem_size;
    const ecs_map_t *map = iter->map;
    if (!ecs_map_is_initialized(map)) {
        return NULL;
    }
    if (iter->bucket == map->buckets_end) {
        return NULL;
    }

    ecs_assert(!elem_size || elem_size == map->elem_size, 
        ECS_INVALID_PARAMETER, NULL);

    ecs_bucket_entry_t *entry = NULL;
    
    if (!iter->bucket) {
        for (iter->bucket = map->buckets; 
            iter->bucket != map->buckets_end; 
            ++iter->bucket) 
        {
            if (iter->bucket->first) {
                entry = iter->bucket->first;
                break;
            }
        }
        if (iter->bucket == map->buckets_end) {
            return NULL;
        }
    } else if ((entry = iter->entry) == NULL) {
        do {
            ++iter->bucket;
            if (iter->bucket == map->buckets_end) {
                return NULL;
            }
        } while(!iter->bucket->first);
        entry = iter->bucket->first;
    }
    
    if (key_out) {
        ecs_assert(entry != NULL, ECS_INTERNAL_ERROR, NULL);
        *key_out = entry->key;
    }

    iter->entry = entry->next;

    return ECS_OFFSET(&entry->key, ECS_SIZEOF(ecs_map_key_t));
}

void* _ecs_map_next_ptr(
    ecs_map_iter_t *iter,
    ecs_map_key_t *key_out)
{
    void *result = _ecs_map_next(iter, ECS_SIZEOF(void*), key_out);
    if (result) {
        return *(void**)result;
    } else {
        return NULL;
    }
}

void ecs_map_grow(
    ecs_map_t *map, 
    int32_t element_count)
{
    ecs_assert(map != NULL, ECS_INVALID_PARAMETER, NULL);
    int32_t target_count = map->count + element_count;
    int32_t bucket_count = get_bucket_count(target_count);

    if (bucket_count > map->bucket_count) {
        rehash(map, bucket_count);
    }
}

void ecs_map_set_size(
    ecs_map_t *map, 
    int32_t element_count)
{    
    ecs_assert(map != NULL, ECS_INVALID_PARAMETER, NULL);
    int32_t bucket_count = get_bucket_count(element_count);

    if (bucket_count) {
        rehash(map, bucket_count);
    }
}

ecs_map_t* ecs_map_copy(
    ecs_map_t *map)
{
    if (!ecs_map_is_initialized(map)) {
        return NULL;
    }

    ecs_size_t elem_size = map->elem_size;
    ecs_map_t *result = _ecs_map_new(map->elem_size, ecs_map_count(map));

    ecs_map_iter_t it = ecs_map_iter(map);
    ecs_map_key_t key;
    void *ptr;
    while ((ptr = _ecs_map_next(&it, elem_size, &key))) {
        _ecs_map_set(result, elem_size, key, ptr);
    }

    return result;
}

void ecs_map_memory(
    ecs_map_t *map, 
    int32_t *allocd,
    int32_t *used)
{
    ecs_assert(map != NULL, ECS_INVALID_PARAMETER, NULL);

    if (used) {
        *used = map->count * map->elem_size;
    }

    // TODO: something something block allocator
    if (allocd) {
        *allocd += ECS_SIZEOF(ecs_map_t);
        *allocd += ECS_SIZEOF(ecs_bucket_entry_t*) * map->bucket_count;
        int32_t index;
        int32_t entry_size = map->elem_size + ECS_SIZEOF(ecs_bucket_entry_t);
        for (index = 0; index < map->bucket_count; ++index) {
            ecs_bucket_entry_t *entry;
            for (entry = map->buckets[index].first; entry; entry = entry->next){
                *allocd += entry_size;
            }
        }
    }
}


static
int32_t find_key(
    const ecs_hashmap_t *map,
    ecs_vector_t *keys,
    ecs_size_t key_size, 
    const void *key)
{
    int32_t i, count = ecs_vector_count(keys);
    void *key_array = ecs_vector_first_t(keys, key_size, 8);
    for (i = 0; i < count; i ++) {
        void *key_ptr = ECS_OFFSET(key_array, key_size * i);
        if (map->compare(key_ptr, key) == 0) {
            return i;
        }
    }
    return -1;
}

void _flecs_hashmap_init(
    ecs_hashmap_t *map,
    ecs_size_t key_size,
    ecs_size_t value_size,
    ecs_hash_value_action_t hash,
    ecs_compare_action_t compare)
{
    map->key_size = key_size;
    map->value_size = value_size;
    map->hash = hash;
    map->compare = compare;
    ecs_map_init(&map->impl, ecs_hm_bucket_t, 0);
}

void flecs_hashmap_fini(
    ecs_hashmap_t *map)
{
    ecs_map_iter_t it = ecs_map_iter(&map->impl);
    ecs_hm_bucket_t *bucket;
    while ((bucket = ecs_map_next(&it, ecs_hm_bucket_t, NULL))) {
        ecs_vector_free(bucket->keys);
        ecs_vector_free(bucket->values);
    }

    ecs_map_fini(&map->impl);
}

void flecs_hashmap_copy(
    const ecs_hashmap_t *src,
    ecs_hashmap_t *dst)
{
    if (dst != src) {
        *dst = *src;
    }
    
    ecs_map_t *impl = ecs_map_copy(&dst->impl);
    dst->impl = *impl;
    ecs_os_free(impl);

    ecs_map_iter_t it = ecs_map_iter(&dst->impl);
    ecs_hm_bucket_t *bucket;
    while ((bucket = ecs_map_next(&it, ecs_hm_bucket_t, NULL))) {
        bucket->keys = ecs_vector_copy_t(bucket->keys, dst->key_size, 8);
        bucket->values = ecs_vector_copy_t(bucket->values, dst->value_size, 8);
    }
}

void* _flecs_hashmap_get(
    const ecs_hashmap_t *map,
    ecs_size_t key_size,
    const void *key,
    ecs_size_t value_size)
{
    ecs_assert(map->key_size == key_size, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(map->value_size == value_size, ECS_INVALID_PARAMETER, NULL);

    uint64_t hash = map->hash(key);
    ecs_hm_bucket_t *bucket = ecs_map_get(&map->impl, ecs_hm_bucket_t, hash);
    if (!bucket) {
        return NULL;
    }

    int32_t index = find_key(map, bucket->keys, key_size, key);
    if (index == -1) {
        return NULL;
    }

    return ecs_vector_get_t(bucket->values, value_size, 8, index);
}

flecs_hashmap_result_t _flecs_hashmap_ensure(
    ecs_hashmap_t *map,
    ecs_size_t key_size,
    const void *key,
    ecs_size_t value_size)
{
    ecs_assert(map->key_size == key_size, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(map->value_size == value_size, ECS_INVALID_PARAMETER, NULL);

    uint64_t hash = map->hash(key);
    ecs_hm_bucket_t *bucket = ecs_map_ensure(&map->impl, ecs_hm_bucket_t, hash);
    ecs_assert(bucket != NULL, ECS_INTERNAL_ERROR, NULL);

    void *value_ptr, *key_ptr;

    ecs_vector_t *keys = bucket->keys;
    if (!keys) {
        bucket->keys = ecs_vector_new_t(key_size, 8, 1);
        bucket->values = ecs_vector_new_t(value_size, 8, 1);
        key_ptr = ecs_vector_add_t(&bucket->keys, key_size, 8);
        ecs_os_memcpy(key_ptr, key, key_size);
        value_ptr = ecs_vector_add_t(&bucket->values, value_size, 8);
        ecs_os_memset(value_ptr, 0, value_size);
    } else {
        int32_t index = find_key(map, keys, key_size, key);
        if (index == -1) {
            key_ptr = ecs_vector_add_t(&bucket->keys, key_size, 8);
            ecs_os_memcpy(key_ptr, key, key_size);
            value_ptr = ecs_vector_add_t(&bucket->values, value_size, 8);
            ecs_assert(value_ptr != NULL, ECS_INTERNAL_ERROR, NULL);
            ecs_os_memset(value_ptr, 0, value_size);
        } else {
            key_ptr = ecs_vector_get_t(bucket->keys, key_size, 8, index);
            value_ptr = ecs_vector_get_t(bucket->values, value_size, 8, index);
        }
    }

    return (flecs_hashmap_result_t){
        .key = key_ptr,
        .value = value_ptr,
        .hash = hash
    };
}

void _flecs_hashmap_set(
    ecs_hashmap_t *map,
    ecs_size_t key_size,
    void *key,
    ecs_size_t value_size,
    const void *value)
{
    void *value_ptr = _flecs_hashmap_ensure(map, key_size, key, value_size).value;
    ecs_assert(value_ptr != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_os_memcpy(value_ptr, value, value_size);
}

ecs_hm_bucket_t* flecs_hashmap_get_bucket(
    const ecs_hashmap_t *map,
    uint64_t hash)
{
    ecs_assert(map != NULL, ECS_INTERNAL_ERROR, NULL);
    return ecs_map_get(&map->impl, ecs_hm_bucket_t, hash);
}

void flecs_hm_bucket_remove(
    ecs_hashmap_t *map,
    ecs_hm_bucket_t *bucket,
    uint64_t hash,
    int32_t index)
{
    ecs_vector_remove_t(bucket->keys, map->key_size, 8, index);
    ecs_vector_remove_t(bucket->values, map->value_size, 8, index);

    if (!ecs_vector_count(bucket->keys)) {
        ecs_vector_free(bucket->keys);
        ecs_vector_free(bucket->values);
        ecs_map_remove(&map->impl, hash);
    }
}

void _flecs_hashmap_remove_w_hash(
    ecs_hashmap_t *map,
    ecs_size_t key_size,
    const void *key,
    ecs_size_t value_size,
    uint64_t hash)
{
    ecs_assert(map->key_size == key_size, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(map->value_size == value_size, ECS_INVALID_PARAMETER, NULL);
    (void)value_size;

    ecs_hm_bucket_t *bucket = ecs_map_get(&map->impl, ecs_hm_bucket_t, hash);
    if (!bucket) {
        return;
    }

    int32_t index = find_key(map, bucket->keys, key_size, key);
    if (index == -1) {
        return;
    }

    flecs_hm_bucket_remove(map, bucket, hash, index);
}

void _flecs_hashmap_remove(
    ecs_hashmap_t *map,
    ecs_size_t key_size,
    const void *key,
    ecs_size_t value_size)
{
    ecs_assert(map->key_size == key_size, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(map->value_size == value_size, ECS_INVALID_PARAMETER, NULL);

    uint64_t hash = map->hash(key);
    _flecs_hashmap_remove_w_hash(map, key_size, key, value_size, hash);
}

flecs_hashmap_iter_t flecs_hashmap_iter(
    ecs_hashmap_t *map)
{
    return (flecs_hashmap_iter_t){
        .it = ecs_map_iter(&map->impl)
    };
}

void* _flecs_hashmap_next(
    flecs_hashmap_iter_t *it,
    ecs_size_t key_size,
    void *key_out,
    ecs_size_t value_size)
{
    int32_t index = ++ it->index;
    ecs_hm_bucket_t *bucket = it->bucket;
    while (!bucket || it->index >= ecs_vector_count(bucket->keys)) {
        bucket = it->bucket = ecs_map_next(&it->it, ecs_hm_bucket_t, NULL);
        if (!bucket) {
            return NULL;
        }
        index = it->index = 0;
    }

    if (key_out) {
        *(void**)key_out = ecs_vector_get_t(bucket->keys, key_size, 8, index);
    }
    
    return ecs_vector_get_t(bucket->values, value_size, 8, index);
}


#define FLECS_STACK_PAGE_OFFSET ECS_ALIGN(ECS_SIZEOF(ecs_stack_page_t), 16)

static
ecs_stack_page_t* flecs_stack_page_new(void) {
    ecs_stack_page_t *result = ecs_os_malloc(
        FLECS_STACK_PAGE_OFFSET + ECS_STACK_PAGE_SIZE);
    result->data = ECS_OFFSET(result, FLECS_STACK_PAGE_OFFSET);
    result->next = NULL;
    return result;
}

void* flecs_stack_alloc(
    ecs_stack_t *stack, 
    ecs_size_t size,
    ecs_size_t align)
{
    ecs_stack_page_t *page = stack->cur;
    if (page == &stack->first && !page->data) {
        page->data = ecs_os_malloc(ECS_STACK_PAGE_SIZE);
    }

    ecs_size_t sp = ECS_ALIGN(page->sp, align);
    ecs_size_t next_sp = sp + size;

    if (next_sp > ECS_STACK_PAGE_SIZE) {
        if (size > ECS_STACK_PAGE_SIZE) {
            return ecs_os_malloc(size); /* Too large for page */
        }

        if (page->next) {
            page = page->next;
        } else {
            page = page->next = flecs_stack_page_new();
        }
        sp = 0;
        next_sp = size;
        stack->cur = page;
    }

    page->sp = next_sp;

    return ECS_OFFSET(page->data, sp);
}

void flecs_stack_free(
    void *ptr,
    ecs_size_t size)
{
    if (size > ECS_STACK_PAGE_SIZE) {
        ecs_os_free(ptr);
    }
}

void flecs_stack_reset(
    ecs_stack_t *stack)
{
    stack->cur = &stack->first;
    stack->first.sp = 0;
}

void flecs_stack_init(
    ecs_stack_t *stack)
{
    ecs_os_zeromem(stack);
    stack->cur = &stack->first;
    stack->first.data = NULL;
}

void flecs_stack_fini(
    ecs_stack_t *stack)
{
    ecs_stack_page_t *next, *cur = &stack->first;
    do {
        next = cur->next;
        if (cur == &stack->first) {
            ecs_os_free(cur->data);
        } else {
            ecs_os_free(cur);
        }
    } while ((cur = next));
}


#ifdef FLECS_LOG

#include <stdio.h>
#include <ctype.h>

static
void ecs_colorize_buf(
    char *msg,
    bool enable_colors,
    ecs_strbuf_t *buf)
{
    char *ptr, ch, prev = '\0';
    bool isNum = false;
    char isStr = '\0';
    bool isVar = false;
    bool overrideColor = false;
    bool autoColor = true;
    bool dontAppend = false;

    for (ptr = msg; (ch = *ptr); ptr++) {
        dontAppend = false;

        if (!overrideColor) {
            if (isNum && !isdigit(ch) && !isalpha(ch) && (ch != '.') && (ch != '%')) {
                if (enable_colors) ecs_strbuf_appendstr(buf, ECS_NORMAL);
                isNum = false;
            }
            if (isStr && (isStr == ch) && prev != '\\') {
                isStr = '\0';
            } else if (((ch == '\'') || (ch == '"')) && !isStr &&
                !isalpha(prev) && (prev != '\\'))
            {
                if (enable_colors) ecs_strbuf_appendstr(buf, ECS_CYAN);
                isStr = ch;
            }

            if ((isdigit(ch) || (ch == '%' && isdigit(prev)) ||
                (ch == '-' && isdigit(ptr[1]))) && !isNum && !isStr && !isVar &&
                 !isalpha(prev) && !isdigit(prev) && (prev != '_') &&
                 (prev != '.'))
            {
                if (enable_colors) ecs_strbuf_appendstr(buf, ECS_GREEN);
                isNum = true;
            }

            if (isVar && !isalpha(ch) && !isdigit(ch) && ch != '_') {
                if (enable_colors) ecs_strbuf_appendstr(buf, ECS_NORMAL);
                isVar = false;
            }

            if (!isStr && !isVar && ch == '$' && isalpha(ptr[1])) {
                if (enable_colors) ecs_strbuf_appendstr(buf, ECS_CYAN);
                isVar = true;
            }
        }

        if (!isVar && !isStr && !isNum && ch == '#' && ptr[1] == '[') {
            bool isColor = true;
            overrideColor = true;

            /* Custom colors */
            if (!ecs_os_strncmp(&ptr[2], "]", ecs_os_strlen("]"))) {
                autoColor = false;
            } else if (!ecs_os_strncmp(&ptr[2], "green]", ecs_os_strlen("green]"))) {
                if (enable_colors) ecs_strbuf_appendstr(buf, ECS_GREEN);
            } else if (!ecs_os_strncmp(&ptr[2], "red]", ecs_os_strlen("red]"))) {
                if (enable_colors) ecs_strbuf_appendstr(buf, ECS_RED);
            } else if (!ecs_os_strncmp(&ptr[2], "blue]", ecs_os_strlen("red]"))) {
                if (enable_colors) ecs_strbuf_appendstr(buf, ECS_BLUE);
            } else if (!ecs_os_strncmp(&ptr[2], "magenta]", ecs_os_strlen("magenta]"))) {
                if (enable_colors) ecs_strbuf_appendstr(buf, ECS_MAGENTA);
            } else if (!ecs_os_strncmp(&ptr[2], "cyan]", ecs_os_strlen("cyan]"))) {
                if (enable_colors) ecs_strbuf_appendstr(buf, ECS_CYAN);
            } else if (!ecs_os_strncmp(&ptr[2], "yellow]", ecs_os_strlen("yellow]"))) {
                if (enable_colors) ecs_strbuf_appendstr(buf, ECS_YELLOW);
            } else if (!ecs_os_strncmp(&ptr[2], "grey]", ecs_os_strlen("grey]"))) {
                if (enable_colors) ecs_strbuf_appendstr(buf, ECS_GREY);
            } else if (!ecs_os_strncmp(&ptr[2], "white]", ecs_os_strlen("white]"))) {
                if (enable_colors) ecs_strbuf_appendstr(buf, ECS_NORMAL);
            } else if (!ecs_os_strncmp(&ptr[2], "bold]", ecs_os_strlen("bold]"))) {
                if (enable_colors) ecs_strbuf_appendstr(buf, ECS_BOLD);
            } else if (!ecs_os_strncmp(&ptr[2], "normal]", ecs_os_strlen("normal]"))) {
                if (enable_colors) ecs_strbuf_appendstr(buf, ECS_NORMAL);
            } else if (!ecs_os_strncmp(&ptr[2], "reset]", ecs_os_strlen("reset]"))) {
                overrideColor = false;
                if (enable_colors) ecs_strbuf_appendstr(buf, ECS_NORMAL);
            } else {
                isColor = false;
                overrideColor = false;
            }

            if (isColor) {
                ptr += 2;
                while ((ch = *ptr) != ']') ptr ++;
                dontAppend = true;
            }
            if (!autoColor) {
                overrideColor = true;
            }
        }

        if (ch == '\n') {
            if (isNum || isStr || isVar || overrideColor) {
                if (enable_colors) ecs_strbuf_appendstr(buf, ECS_NORMAL);
                overrideColor = false;
                isNum = false;
                isStr = false;
                isVar = false;
            }
        }

        if (!dontAppend) {
            ecs_strbuf_appendstrn(buf, ptr, 1);
        }

        if (!overrideColor) {
            if (((ch == '\'') || (ch == '"')) && !isStr) {
                if (enable_colors) ecs_strbuf_appendstr(buf, ECS_NORMAL);
            }
        }

        prev = ch;
    }

    if (isNum || isStr || isVar || overrideColor) {
        if (enable_colors) ecs_strbuf_appendstr(buf, ECS_NORMAL);
    }
}

void _ecs_logv(
    int level,
    const char *file,
    int32_t line,
    const char *fmt,
    va_list args)
{
    (void)level;
    (void)line;

    ecs_strbuf_t msg_buf = ECS_STRBUF_INIT;

    if (level > ecs_os_api.log_level_) {
        return;
    }

    /* Apply color. Even if we don't want color, we still need to call the
     * colorize function to get rid of the color tags (e.g. #[green]) */
    char *msg_nocolor = ecs_vasprintf(fmt, args);
    ecs_colorize_buf(msg_nocolor, 
        ecs_os_api.flags_ & EcsOsApiLogWithColors, &msg_buf);
    ecs_os_free(msg_nocolor);
    
    char *msg = ecs_strbuf_get(&msg_buf);
    if (msg) {
        ecs_os_api.log_(level, file, line, msg);
        ecs_os_free(msg);
    } else {
        ecs_os_api.log_(level, file, line, "");
    }
}

void _ecs_log(
    int level,
    const char *file,
    int32_t line,
    const char *fmt,
    ...)
{
    va_list args;
    va_start(args, fmt);
    _ecs_logv(level, file, line, fmt, args);
    va_end(args);    
}

void _ecs_log_push(
    int32_t level) 
{
    if (level <= ecs_os_api.log_level_) {
        ecs_os_api.log_indent_ ++;
    }
}

void _ecs_log_pop(
    int32_t level)
{
    if (level <= ecs_os_api.log_level_) {
        ecs_os_api.log_indent_ --;
    }
}

void _ecs_parser_errorv(
    const char *name,
    const char *expr, 
    int64_t column_arg,
    const char *fmt,
    va_list args)
{
    int32_t column = flecs_itoi32(column_arg);

    if (ecs_os_api.log_level_ >= -2) {
        ecs_strbuf_t msg_buf = ECS_STRBUF_INIT;

        ecs_strbuf_vappend(&msg_buf, fmt, args);

        if (expr) {
            ecs_strbuf_appendstr(&msg_buf, "\n");

            /* Find start of line by taking column and looking for the
             * last occurring newline */
            if (column != -1) {
                const char *ptr = &expr[column];
                while (ptr[0] != '\n' && ptr > expr) {
                    ptr --;
                }

                if (ptr == expr) {
                    /* ptr is already at start of line */
                } else {
                    column -= (int32_t)(ptr - expr + 1);
                    expr = ptr + 1;
                }
            }

            /* Strip newlines from current statement, if any */            
            char *newline_ptr = strchr(expr, '\n');
            if (newline_ptr) {
                /* Strip newline from expr */
                ecs_strbuf_appendstrn(&msg_buf, expr, 
                    (int32_t)(newline_ptr - expr));
            } else {
                ecs_strbuf_appendstr(&msg_buf, expr);
            }

            ecs_strbuf_appendstr(&msg_buf, "\n");

            if (column != -1) {
                ecs_strbuf_append(&msg_buf, "%*s^", column, "");
            }
        }

        char *msg = ecs_strbuf_get(&msg_buf);
        ecs_os_err(name, 0, msg);
        ecs_os_free(msg);
    }
}

void _ecs_parser_error(
    const char *name,
    const char *expr, 
    int64_t column,
    const char *fmt,
    ...)
{
    if (ecs_os_api.log_level_  >= -2) {
        va_list args;
        va_start(args, fmt);
        _ecs_parser_errorv(name, expr, column, fmt, args);
        va_end(args);
    }
}

void _ecs_abort(
    int32_t err,
    const char *file,
    int32_t line,
    const char *fmt,
    ...)
{
    if (fmt) {
        va_list args;
        va_start(args, fmt);
        char *msg = ecs_vasprintf(fmt, args);
        va_end(args);
        _ecs_fatal(file, line, "%s (%s)", msg, ecs_strerror(err));
        ecs_os_free(msg);
    } else {
        _ecs_fatal(file, line, "%s", ecs_strerror(err));
    }
    ecs_os_api.log_last_error_ = err;
}

bool _ecs_assert(
    bool condition,
    int32_t err,
    const char *cond_str,
    const char *file,
    int32_t line,
    const char *fmt,
    ...)
{
    if (!condition) {
        if (fmt) {
            va_list args;
            va_start(args, fmt);
            char *msg = ecs_vasprintf(fmt, args);
            va_end(args);            
            _ecs_fatal(file, line, "assert: %s %s (%s)", 
                cond_str, msg, ecs_strerror(err));
            ecs_os_free(msg);
        } else {
            _ecs_fatal(file, line, "assert: %s %s", 
                cond_str, ecs_strerror(err));
        }
        ecs_os_api.log_last_error_ = err;
    }

    return condition;
}

void _ecs_deprecated(
    const char *file,
    int32_t line,
    const char *msg)
{
    _ecs_err(file, line, "%s", msg);
}

bool ecs_should_log(int32_t level) {
#   if !defined(FLECS_LOG_3)
    if (level == 3) {
        return false;
    }
#   endif
#   if !defined(FLECS_LOG_2)
    if (level == 2) {
        return false;
    }
#   endif
#   if !defined(FLECS_LOG_1)
    if (level == 1) {
        return false;
    }
#   endif

    return level <= ecs_os_api.log_level_;
}

#define ECS_ERR_STR(code) case code: return &(#code[4])

const char* ecs_strerror(
    int32_t error_code)
{
    switch (error_code) {
    ECS_ERR_STR(ECS_INVALID_PARAMETER);
    ECS_ERR_STR(ECS_NOT_A_COMPONENT);
    ECS_ERR_STR(ECS_INTERNAL_ERROR);
    ECS_ERR_STR(ECS_ALREADY_DEFINED);
    ECS_ERR_STR(ECS_INVALID_COMPONENT_SIZE);
    ECS_ERR_STR(ECS_INVALID_COMPONENT_ALIGNMENT);
    ECS_ERR_STR(ECS_NAME_IN_USE);
    ECS_ERR_STR(ECS_OUT_OF_MEMORY);
    ECS_ERR_STR(ECS_OPERATION_FAILED);
    ECS_ERR_STR(ECS_INVALID_CONVERSION);
    ECS_ERR_STR(ECS_MODULE_UNDEFINED);
    ECS_ERR_STR(ECS_MISSING_SYMBOL);
    ECS_ERR_STR(ECS_ALREADY_IN_USE);
    ECS_ERR_STR(ECS_CYCLE_DETECTED);
    ECS_ERR_STR(ECS_COLUMN_INDEX_OUT_OF_RANGE);
    ECS_ERR_STR(ECS_COLUMN_IS_NOT_SHARED);
    ECS_ERR_STR(ECS_COLUMN_IS_SHARED);
    ECS_ERR_STR(ECS_COLUMN_TYPE_MISMATCH);
    ECS_ERR_STR(ECS_INVALID_WHILE_READONLY);
    ECS_ERR_STR(ECS_INVALID_FROM_WORKER);
    ECS_ERR_STR(ECS_OUT_OF_RANGE);
    ECS_ERR_STR(ECS_MISSING_OS_API);
    ECS_ERR_STR(ECS_UNSUPPORTED);
    ECS_ERR_STR(ECS_ACCESS_VIOLATION);
    ECS_ERR_STR(ECS_COMPONENT_NOT_REGISTERED);
    ECS_ERR_STR(ECS_INCONSISTENT_COMPONENT_ID);
    ECS_ERR_STR(ECS_INCONSISTENT_NAME);
    ECS_ERR_STR(ECS_INCONSISTENT_COMPONENT_ACTION);
    ECS_ERR_STR(ECS_INVALID_OPERATION);
    ECS_ERR_STR(ECS_CONSTRAINT_VIOLATED);
    ECS_ERR_STR(ECS_LOCKED_STORAGE);
    ECS_ERR_STR(ECS_ID_IN_USE);
    }

    return "unknown error code";
}

#else

/* Empty bodies for when logging is disabled */

void _ecs_log(
    int32_t level,
    const char *file,
    int32_t line,
    const char *fmt,
    ...)
{
    (void)level;
    (void)file;
    (void)line;
    (void)fmt;
}

void _ecs_parser_error(
    const char *name,
    const char *expr, 
    int64_t column,
    const char *fmt,
    ...)
{
    (void)name;
    (void)expr;
    (void)column;
    (void)fmt;
}

void _ecs_parser_errorv(
    const char *name,
    const char *expr, 
    int64_t column,
    const char *fmt,
    va_list args)
{
    (void)name;
    (void)expr;
    (void)column;
    (void)fmt;
    (void)args;
}

void _ecs_abort(
    int32_t error_code,
    const char *file,
    int32_t line,
    const char *fmt,
    ...)
{
    (void)error_code;
    (void)file;
    (void)line;
    (void)fmt;
}

bool _ecs_assert(
    bool condition,
    int32_t error_code,
    const char *condition_str,
    const char *file,
    int32_t line,
    const char *fmt,
    ...)
{
    (void)condition;
    (void)error_code;
    (void)condition_str;
    (void)file;
    (void)line;
    (void)fmt;
    return true;
}

#endif

int ecs_log_set_level(
    int level)
{
    int prev = level;
    ecs_os_api.log_level_ = level;
    return prev;
}

bool ecs_log_enable_colors(
    bool enabled)
{
    bool prev = ecs_os_api.flags_ & EcsOsApiLogWithColors;
    ECS_BIT_COND(ecs_os_api.flags_, EcsOsApiLogWithColors, enabled);
    return prev;
}

bool ecs_log_enable_timestamp(
    bool enabled)
{
    bool prev = ecs_os_api.flags_ & EcsOsApiLogWithTimeStamp;
    ECS_BIT_COND(ecs_os_api.flags_, EcsOsApiLogWithTimeStamp, enabled);
    return prev;
}

bool ecs_log_enable_timedelta(
    bool enabled)
{
    bool prev = ecs_os_api.flags_ & EcsOsApiLogWithTimeDelta;
    ECS_BIT_COND(ecs_os_api.flags_, EcsOsApiLogWithTimeDelta, enabled);
    return prev;
}

int ecs_log_last_error(void)
{
    int result = ecs_os_api.log_last_error_;
    ecs_os_api.log_last_error_ = 0;
    return result;
}

#ifndef FLECS_SYSTEM_PRIVATE_H
#define FLECS_SYSTEM_PRIVATE_H

#ifdef FLECS_SYSTEM


#define ecs_system_t_magic     (0x65637383)
#define ecs_system_t_tag       EcsSystem

extern ecs_mixins_t ecs_system_t_mixins;

typedef struct ecs_system_t {
    ecs_header_t hdr;

    ecs_run_action_t run;           /* See ecs_system_desc_t */
    ecs_iter_action_t action;       /* See ecs_system_desc_t */

    ecs_query_t *query;             /* System query */
    ecs_entity_t query_entity;      /* Entity associated with query */
    ecs_entity_t tick_source;       /* Tick source associated with system */
    
    /* Schedule parameters */
    bool multi_threaded;
    bool no_staging;

    int32_t invoke_count;           /* Number of times system is invoked */
    float time_spent;               /* Time spent on running system */
    ecs_ftime_t time_passed;        /* Time passed since last invocation */
    int32_t last_frame;             /* Last frame for which the system was considered */

    void *ctx;                      /* Userdata for system */
    void *binding_ctx;              /* Optional language binding context */

    ecs_ctx_free_t ctx_free;
    ecs_ctx_free_t binding_ctx_free;

    /* Mixins */
    ecs_world_t *world;
    ecs_entity_t entity;
    ecs_poly_dtor_t dtor;      
} ecs_system_t;

/* Invoked when system becomes active / inactive */
void ecs_system_activate(
    ecs_world_t *world,
    ecs_entity_t system,
    bool activate,
    const ecs_system_t *system_data);

/* Internal function to run a system */
ecs_entity_t ecs_run_intern(
    ecs_world_t *world,
    ecs_stage_t *stage,
    ecs_entity_t system,
    ecs_system_t *system_data,
    int32_t stage_current,
    int32_t stage_count,
    ecs_ftime_t delta_time,
    int32_t offset,
    int32_t limit,
    void *param);

#endif

#endif


#ifdef FLECS_PIPELINE
#ifndef FLECS_PIPELINE_PRIVATE_H
#define FLECS_PIPELINE_PRIVATE_H


/** Instruction data for pipeline.
 * This type is the element type in the "ops" vector of a pipeline and contains
 * information about the set of systems that need to be ran before a merge. */
typedef struct ecs_pipeline_op_t {
    int32_t count;              /* Number of systems to run before merge */
    bool multi_threaded;        /* Whether systems can be ran multi threaded */
    bool no_staging;            /* Whether systems are staged or not */
} ecs_pipeline_op_t;

typedef struct {
    ecs_query_t *query;         /* Pipeline query */
    
    ecs_vector_t *ops;          /* Pipeline schedule */
    int32_t match_count;        /* Used to track of rebuild is necessary */
    int32_t rebuild_count;      /* Number of pipeline rebuilds */
    ecs_entity_t last_system;   /* Last system ran by pipeline */

    ecs_id_record_t *idr_inactive; /* Cached record for quick inactive test */

    ecs_iter_t *iters;          /* Iterator for worker(s) */
    int32_t iter_count;

    /* Members for continuing pipeline iteration after pipeline rebuild */
    ecs_pipeline_op_t *cur_op;  /* Current pipeline op */
    int32_t cur_i;              /* Index in current result */
} EcsPipeline;

////////////////////////////////////////////////////////////////////////////////
//// Pipeline API
////////////////////////////////////////////////////////////////////////////////

/** Update a pipeline (internal function).
 * Before running a pipeline, it must be updated. During this update phase
 * all systems in the pipeline are collected, ordered and sync points are 
 * inserted where necessary. This operation may only be called when staging is
 * disabled.
 *
 * Because multiple threads may run a pipeline, preparing the pipeline must 
 * happen synchronously, which is why this function is separate from 
 * ecs_run_pipeline. Not running the prepare step may cause systems to not get
 * ran, or ran in the wrong order.
 *
 * If 0 is provided for the pipeline id, the default pipeline will be ran (this
 * is either the builtin pipeline or the pipeline set with set_pipeline()).
 * 
 * @param world The world.
 * @param pipeline The pipeline to run.
 * @return The number of elements in the pipeline.
 */
bool ecs_pipeline_update(
    ecs_world_t *world,
    ecs_entity_t pipeline,
    bool start_of_frame); 

void ecs_pipeline_reset_iter(
    ecs_world_t *world,
    EcsPipeline *q);

////////////////////////////////////////////////////////////////////////////////
//// Worker API
////////////////////////////////////////////////////////////////////////////////

void ecs_worker_begin(
    ecs_world_t *world);

bool ecs_worker_sync(
    ecs_world_t *world,
    EcsPipeline *p);

void ecs_worker_end(
    ecs_world_t *world);

void ecs_workers_progress(
    ecs_world_t *world,
    ecs_entity_t pipeline,
    ecs_ftime_t delta_time);

#endif


/* Worker thread */
static
void* worker(void *arg) {
    ecs_stage_t *stage = arg;
    ecs_world_t *world = stage->world;

    ecs_dbg_2("worker %d: start", stage->id);

    /* Start worker, increase counter so main thread knows how many
     * workers are ready */
    ecs_os_mutex_lock(world->sync_mutex);
    world->workers_running ++;

    if (!(world->flags & EcsWorldQuitWorkers)) {
        ecs_os_cond_wait(world->worker_cond, world->sync_mutex);
    }

    ecs_os_mutex_unlock(world->sync_mutex);

    while (!(world->flags & EcsWorldQuitWorkers)) {
        ecs_entity_t old_scope = ecs_set_scope((ecs_world_t*)stage, 0);

        ecs_dbg_3("worker %d: run", stage->id);
 
        ecs_run_pipeline(
            (ecs_world_t*)stage, 
            world->pipeline, 
            world->info.delta_time);

        ecs_set_scope((ecs_world_t*)stage, old_scope);
    }

    ecs_dbg_2("worker %d: finalizing", stage->id);

    ecs_os_mutex_lock(world->sync_mutex);
    world->workers_running --;
    ecs_os_mutex_unlock(world->sync_mutex);

    ecs_dbg_2("worker %d: stop", stage->id);

    return NULL;
}

/* Start threads */
static
void start_workers(
    ecs_world_t *world,
    int32_t threads)
{
    ecs_set_stage_count(world, threads);

    ecs_assert(ecs_get_stage_count(world) == threads, ECS_INTERNAL_ERROR, NULL);

    int32_t i;
    for (i = 0; i < threads; i ++) {
        ecs_stage_t *stage = (ecs_stage_t*)ecs_get_stage(world, i);
        ecs_assert(stage != NULL, ECS_INTERNAL_ERROR, NULL);
        ecs_poly_assert(stage, ecs_stage_t);
        stage->thread = ecs_os_thread_new(worker, stage);
        ecs_assert(stage->thread != 0, ECS_OPERATION_FAILED, NULL);
    }
}

/* Wait until all workers are running */
static
void wait_for_workers(
    ecs_world_t *world)
{
    int32_t stage_count = ecs_get_stage_count(world);
    bool wait = true;

    do {
        ecs_os_mutex_lock(world->sync_mutex);
        if (world->workers_running == stage_count) {
            wait = false;
        }
        ecs_os_mutex_unlock(world->sync_mutex);
    } while (wait);
}

/* Synchronize workers */
static
void sync_worker(
    ecs_world_t *world)
{
    int32_t stage_count = ecs_get_stage_count(world);

    /* Signal that thread is waiting */
    ecs_os_mutex_lock(world->sync_mutex);
    if (++ world->workers_waiting == stage_count) {
        /* Only signal main thread when all threads are waiting */
        ecs_os_cond_signal(world->sync_cond);
    }

    /* Wait until main thread signals that thread can continue */
    ecs_os_cond_wait(world->worker_cond, world->sync_mutex);
    ecs_os_mutex_unlock(world->sync_mutex);
}

/* Wait until all threads are waiting on sync point */
static
void wait_for_sync(
    ecs_world_t *world)
{
    int32_t stage_count = ecs_get_stage_count(world);

    ecs_dbg_3("#[bold]pipeline: waiting for worker sync");

    ecs_os_mutex_lock(world->sync_mutex);
    if (world->workers_waiting != stage_count) {
        ecs_os_cond_wait(world->sync_cond, world->sync_mutex);
    }
    
    /* We should have been signalled unless all workers are waiting on sync */
    ecs_assert(world->workers_waiting == stage_count, 
        ECS_INTERNAL_ERROR, NULL);

    ecs_os_mutex_unlock(world->sync_mutex);

    ecs_dbg_3("#[bold]pipeline: workers synced");
}

/* Signal workers that they can start/resume work */
static
void signal_workers(
    ecs_world_t *world)
{
    ecs_dbg_3("#[bold]pipeline: signal workers");
    ecs_os_mutex_lock(world->sync_mutex);
    ecs_os_cond_broadcast(world->worker_cond);
    ecs_os_mutex_unlock(world->sync_mutex);
}

/** Stop workers */
static
bool ecs_stop_threads(
    ecs_world_t *world)
{
    bool threads_active = false;

    /* Test if threads are created. Cannot use workers_running, since this is
     * a potential race if threads haven't spun up yet. */
    ecs_stage_t *stages = world->stages;
    int i, count = world->stage_count;
    for (i = 0; i < count; i ++) {
        ecs_stage_t *stage = &stages[i];
        if (stage->thread) {
            threads_active = true;
            break;
        }
        stage->thread = 0;
    };

    /* If no threads are active, just return */
    if (!threads_active) {
        return false;
    }

    /* Make sure all threads are running, to ensure they catch the signal */
    wait_for_workers(world);

    /* Signal threads should quit */
    world->flags |= EcsWorldQuitWorkers;
    signal_workers(world);

    /* Join all threads with main */
    for (i = 0; i < count; i ++) {
        ecs_os_thread_join(stages[i].thread);
        stages[i].thread = 0;
    }

    world->flags &= ~EcsWorldQuitWorkers;
    ecs_assert(world->workers_running == 0, ECS_INTERNAL_ERROR, NULL);

    /* Deinitialize stages */
    ecs_set_stage_count(world, 1);

    return true;
}

/* -- Private functions -- */

void ecs_worker_begin(
    ecs_world_t *world)
{
    flecs_stage_from_world(&world);
    int32_t stage_count = ecs_get_stage_count(world);
    ecs_assert(stage_count != 0, ECS_INTERNAL_ERROR, NULL);
    
    if (stage_count == 1) {
        ecs_entity_t pipeline = world->pipeline;
        const EcsPipeline *pq = ecs_get(world, pipeline, EcsPipeline);
        ecs_assert(pq != NULL, ECS_INTERNAL_ERROR, NULL);

        ecs_pipeline_op_t *op = ecs_vector_first(pq->ops, ecs_pipeline_op_t);
        if (!op || !op->no_staging) {
            ecs_readonly_begin(world);
        }
    }
}

bool ecs_worker_sync(
    ecs_world_t *world,
    EcsPipeline *pq)
{
    ecs_assert(pq != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(pq->cur_op != NULL, ECS_INTERNAL_ERROR, NULL);
    bool rebuild = false;

    int32_t stage_count = ecs_get_stage_count(world);
    ecs_assert(stage_count != 0, ECS_INTERNAL_ERROR, NULL);
    int32_t build_count = world->info.pipeline_build_count_total;

    /* If there are no threads, merge in place */
    if (stage_count == 1) {
        if (!pq->cur_op->no_staging) {
            ecs_readonly_end(world);
        }

        ecs_pipeline_update(world, world->pipeline, false);

    /* Synchronize all workers. The last worker to reach the sync point will
     * signal the main thread, which will perform the merge. */
    } else {
        sync_worker(world);
    }

    if (build_count != world->info.pipeline_build_count_total) {
        rebuild = true;
    }

    if (stage_count == 1) {
        if (!pq->cur_op->no_staging) {
            ecs_readonly_begin(world);
        }
    }

    return rebuild;
}

void ecs_worker_end(
    ecs_world_t *world)
{
    flecs_stage_from_world(&world);

    int32_t stage_count = ecs_get_stage_count(world);
    ecs_assert(stage_count != 0, ECS_INTERNAL_ERROR, NULL);

    /* If there are no threads, merge in place */
    if (stage_count == 1) {
        if (ecs_stage_is_readonly(world)) {
            ecs_readonly_end(world);
        }

    /* Synchronize all workers. The last worker to reach the sync point will
     * signal the main thread, which will perform the merge. */
    } else {
        sync_worker(world);
    }
}

void ecs_workers_progress(
    ecs_world_t *world,
    ecs_entity_t pipeline,
    ecs_ftime_t delta_time)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_assert(!ecs_is_deferred(world), ECS_INVALID_OPERATION, NULL);
    int32_t stage_count = ecs_get_stage_count(world);

    ecs_time_t start = {0};
    if (world->flags & EcsWorldMeasureFrameTime) {
        ecs_time_measure(&start);
    }

    EcsPipeline *pq = ecs_get_mut(world, pipeline, EcsPipeline);
    ecs_assert(pq != NULL, ECS_INTERNAL_ERROR, NULL);

    if (stage_count != pq->iter_count) {
        pq->iters = ecs_os_realloc_n(pq->iters, ecs_iter_t, stage_count);
        pq->iter_count = stage_count;
    }

    ecs_pipeline_update(world, pipeline, true);
    ecs_vector_t *ops = pq->ops;
    ecs_pipeline_op_t *op = ecs_vector_first(ops, ecs_pipeline_op_t);
    if (!op) {
        return;
    }

    if (stage_count == 1) {
        ecs_entity_t old_scope = ecs_set_scope(world, 0);
        ecs_world_t *stage = ecs_get_stage(world, 0);
        ecs_run_pipeline(stage, pipeline, delta_time);
        ecs_set_scope(world, old_scope);
    } else {
        ecs_pipeline_op_t *op_last = ecs_vector_last(ops, ecs_pipeline_op_t);

        /* Make sure workers are running and ready */
        wait_for_workers(world);

        /* Synchronize n times for each op in the pipeline */
        for (; op <= op_last; op ++) {
            if (!op->no_staging) {
                ecs_readonly_begin(world);
            }

            /* Signal workers that they should start running systems */
            world->workers_waiting = 0;
            signal_workers(world);

            /* Wait until all workers are waiting on sync point */
            wait_for_sync(world);

            /* Merge */
            if (!op->no_staging) {
                ecs_readonly_end(world);
            }

            if (ecs_pipeline_update(world, pipeline, false)) {
                ecs_assert(!ecs_is_deferred(world), ECS_INVALID_OPERATION, NULL);
                pq = ecs_get_mut(world, pipeline, EcsPipeline);
                /* Refetch, in case pipeline itself has moved */
                op = pq->cur_op - 1;
                op_last = ecs_vector_last(pq->ops, ecs_pipeline_op_t);
                ecs_assert(op <= op_last, ECS_INTERNAL_ERROR, NULL);
            }
        }
    }

    if (world->flags & EcsWorldMeasureFrameTime) {
        world->info.system_time_total += (float)ecs_time_measure(&start);
    }    
}

/* -- Public functions -- */

void ecs_set_threads(
    ecs_world_t *world,
    int32_t threads)
{
    ecs_assert(threads <= 1 || ecs_os_has_threading(), ECS_MISSING_OS_API, NULL);

    int32_t stage_count = ecs_get_stage_count(world);

    if (stage_count != threads) {
        /* Stop existing threads */
        if (stage_count > 1) {
            if (ecs_stop_threads(world)) {
                ecs_os_cond_free(world->worker_cond);
                ecs_os_cond_free(world->sync_cond);
                ecs_os_mutex_free(world->sync_mutex);
            }
        }

        /* Start threads if number of threads > 1 */
        if (threads > 1) {
            world->worker_cond = ecs_os_cond_new();
            world->sync_cond = ecs_os_cond_new();
            world->sync_mutex = ecs_os_mutex_new();
            start_workers(world, threads);
        }
    }
}

#endif


#ifdef FLECS_PIPELINE

static ECS_DTOR(EcsPipeline, ptr, {
    ecs_vector_free(ptr->ops);
    ecs_os_free(ptr->iters);
})

typedef enum ComponentWriteState {
    NotWritten = 0,
    WriteToMain,
    WriteToStage
} ComponentWriteState;

typedef struct write_state_t {
    ecs_map_t *components;
    bool wildcard;
} write_state_t;

static
int32_t get_write_state(
    ecs_map_t *write_state,
    ecs_entity_t component)
{
    int32_t *ptr = ecs_map_get(write_state, int32_t, component);
    if (ptr) {
        return *ptr;
    } else {
        return 0;
    }
}

static
void set_write_state(
    write_state_t *write_state,
    ecs_entity_t component,
    int32_t value)
{
    if (component == EcsWildcard) {
        ecs_assert(value == WriteToStage, ECS_INTERNAL_ERROR, NULL);
        write_state->wildcard = true;
    } else {
        ecs_map_set(write_state->components, component, &value);
    }
}

static
void reset_write_state(
    write_state_t *write_state)
{
    ecs_map_clear(write_state->components);
    write_state->wildcard = false;
}

static
int32_t get_any_write_state(
    write_state_t *write_state)
{
    if (write_state->wildcard) {
        return WriteToStage;
    }

    ecs_map_iter_t it = ecs_map_iter(write_state->components);
    int32_t *elem;
    while ((elem = ecs_map_next(&it, int32_t, NULL))) {
        if (*elem == WriteToStage) {
            return WriteToStage;
        }
    }

    return 0;
}

static
bool check_term_component(
    ecs_term_t *term,
    bool is_active,
    ecs_entity_t component,
    write_state_t *write_state)    
{
    int32_t state = get_write_state(write_state->components, component);

    ecs_term_id_t *src = &term->src;

    if ((src->flags & EcsSelf) && ecs_term_match_this(term) && term->oper != EcsNot) {
        switch(term->inout) {
        case EcsInOutNone:
            /* Ignore terms that aren't read/written */
            break;
        case EcsInOutDefault:
        case EcsInOut:
        case EcsIn:
            if (state == WriteToStage || write_state->wildcard) {
                return true;
            }
            // fall through
        case EcsOut:
            if (is_active && term->inout != EcsIn) {
                set_write_state(write_state, component, WriteToMain);
            }
        };
    } else if (!src->id || term->oper == EcsNot) {
        bool needs_merge = false;

        switch(term->inout) {
        case EcsInOutDefault:
        case EcsIn:
        case EcsInOut:
            if (state == WriteToStage) {
                needs_merge = true;
            }
            if (component == EcsWildcard) {
                if (get_any_write_state(write_state) == WriteToStage) {
                    needs_merge = true;
                }
            }
            break;
        default:
            break;
        };

        switch(term->inout) {
        case EcsInOutDefault:
            if ((!(src->flags & EcsSelf) || (!ecs_term_match_this(term))) && (!ecs_term_match_0(term))) {
                /* Default inout behavior is [inout] for This terms, and [in]
                 * for terms that match other entities */
                break;
            }
            // fall through
        case EcsInOut:
        case EcsOut:
            if (is_active) {
                set_write_state(write_state, component, WriteToStage);
            }
            break;
        default:
            break;
        };   

        if (needs_merge) {
            return true;
        }
    }

    return false;
}

static
bool check_term(
    ecs_term_t *term,
    bool is_active,
    write_state_t *write_state)
{
    if (term->oper != EcsOr) {
        return check_term_component(
            term, is_active, term->id, write_state);
    }  

    return false;
}

static
bool check_terms(
    ecs_filter_t *filter,
    bool is_active,
    write_state_t *ws)
{
    bool needs_merge = false;
    ecs_term_t *terms = filter->terms;
    int32_t t, term_count = filter->term_count;

    /* Check This terms first. This way if a term indicating writing to a stage
     * was added before the term, it won't cause merging. */
    for (t = 0; t < term_count; t ++) {
        ecs_term_t *term = &terms[t];
        if (ecs_term_match_this(term)) {
            needs_merge |= check_term(term, is_active, ws);
        }
    }

    /* Now check staged terms */
    for (t = 0; t < term_count; t ++) {
        ecs_term_t *term = &terms[t];
        if (!ecs_term_match_this(term)) {
            needs_merge |= check_term(term, is_active, ws);
        }
    }

    return needs_merge;
}

static
bool flecs_pipeline_is_inactive(
    const EcsPipeline *pq,
    ecs_table_t *table)
{
    return flecs_id_record_get_table(pq->idr_inactive, table) != NULL;
}

static
EcsPoly* flecs_pipeline_term_system(
    ecs_iter_t *it)
{
    int32_t index = ecs_search(it->world, it->table, ecs_poly_id(EcsSystem), 0);
    ecs_assert(index != -1, ECS_INTERNAL_ERROR, NULL);
    EcsPoly *poly = ecs_table_get_column(it->table, index);
    ecs_assert(poly != NULL, ECS_INTERNAL_ERROR, NULL);
    poly = &poly[it->offset];
    return poly;
}

static
bool flecs_pipeline_build(
    ecs_world_t *world,
    ecs_entity_t pipeline,
    EcsPipeline *pq)
{
    (void)pipeline;

    ecs_query_iter(world, pq->query);

    if (pq->match_count == pq->query->match_count) {
        /* No need to rebuild the pipeline */
        return false;
    }

    world->info.pipeline_build_count_total ++;
    pq->rebuild_count ++;

    write_state_t ws = {
        .components = ecs_map_new(int32_t, ECS_HI_COMPONENT_ID),
        .wildcard = false
    };

    ecs_pipeline_op_t *op = NULL;
    ecs_vector_t *ops = NULL;
    ecs_query_t *query = pq->query;

    if (pq->ops) {
        ecs_vector_free(pq->ops);
    }

    bool multi_threaded = false;
    bool no_staging = false;
    bool first = true;

    /* Iterate systems in pipeline, add ops for running / merging */
    ecs_iter_t it = ecs_query_iter(world, query);
    while (ecs_query_next(&it)) {
        EcsPoly *poly = flecs_pipeline_term_system(&it);

        int i;
        for (i = 0; i < it.count; i ++) {
            ecs_system_t *sys = ecs_poly(poly[i].poly, ecs_system_t);
            ecs_query_t *q = sys->query;
            if (!q) {
                continue;
            }

            bool needs_merge = false;
            bool is_active = !ecs_has_id(
                world, it.entities[i], EcsEmpty);
            needs_merge = check_terms(&q->filter, is_active, &ws);

            if (is_active) {
                if (first) {
                    multi_threaded = sys->multi_threaded;
                    no_staging = sys->no_staging;
                    first = false;
                }

                if (sys->multi_threaded != multi_threaded) {
                    needs_merge = true;
                    multi_threaded = sys->multi_threaded;
                }
                if (sys->no_staging != no_staging) {
                    needs_merge = true;
                    no_staging = sys->no_staging;
                }
            }

            if (needs_merge) {
                /* After merge all components will be merged, so reset state */
                reset_write_state(&ws);
                op = NULL;

                /* Re-evaluate columns to set write flags if system is active.
                 * If system is inactive, it can't write anything and so it
                 * should not insert unnecessary merges.  */
                needs_merge = false;
                if (is_active) {
                    needs_merge = check_terms(&q->filter, true, &ws);
                }

                /* The component states were just reset, so if we conclude that
                 * another merge is needed something is wrong. */
                ecs_assert(needs_merge == false, ECS_INTERNAL_ERROR, NULL);        
            }

            if (!op) {
                op = ecs_vector_add(&ops, ecs_pipeline_op_t);
                op->count = 0;
                op->multi_threaded = false;
                op->no_staging = false;
            }

            /* Don't increase count for inactive systems, as they are ignored by
             * the query used to run the pipeline. */
            if (is_active) {
                if (!op->count) {
                    op->multi_threaded = multi_threaded;
                    op->no_staging = no_staging;
                }
                op->count ++;
            }
        }
    }

    ecs_map_free(ws.components);

    /* Find the system ran last this frame (helps workers reset iter) */
    ecs_entity_t last_system = 0;
    op = ecs_vector_first(ops, ecs_pipeline_op_t);
    int32_t i, ran_since_merge = 0, op_index = 0;

    if (!op) {
        ecs_dbg("#[green]pipeline#[reset] is empty");
        return true;
    } else {
        ecs_assert(op != NULL, ECS_INTERNAL_ERROR, NULL);

        /* Add schedule to debug tracing */
        ecs_dbg("#[bold]pipeline rebuild");
        ecs_log_push_1();

        ecs_dbg("#[green]schedule#[reset]: threading: %d, staging: %d:", 
            op->multi_threaded, !op->no_staging);
        ecs_log_push_1();

        it = ecs_query_iter(world, pq->query);
        while (ecs_query_next(&it)) {
            if (flecs_pipeline_is_inactive(pq, it.table)) {
                continue;
            }

            EcsPoly *poly = flecs_pipeline_term_system(&it);

            for (i = 0; i < it.count; i ++) {
                ecs_system_t *sys = ecs_poly(poly[i].poly, ecs_system_t);
                if (ecs_should_log_1()) {
                    char *path = ecs_get_fullpath(world, it.entities[i]);
                    ecs_dbg("#[green]system#[reset] %s", path);
                    ecs_os_free(path);
                }

                ran_since_merge ++;
                if (ran_since_merge == op[op_index].count) {
                    ecs_dbg("#[magenta]merge#[reset]");
                    ecs_log_pop_1();
                    ran_since_merge = 0;
                    op_index ++;
                    if (op_index < ecs_vector_count(ops)) {
                        ecs_dbg(
                            "#[green]schedule#[reset]: "
                            "threading: %d, staging: %d:",
                            op[op_index].multi_threaded, 
                            !op[op_index].no_staging);
                    }
                    ecs_log_push_1();
                }

                if (sys->last_frame == (world->info.frame_count_total + 1)) {
                    last_system = it.entities[i];

                    /* Can't break from loop yet. It's possible that previously
                     * inactive systems that ran before the last ran system are 
                     * now active. */
                }
            }
        }

        ecs_log_pop_1();
        ecs_log_pop_1();
    }

    /* Force sort of query as this could increase the match_count */
    pq->match_count = pq->query->match_count;
    pq->ops = ops;
    pq->last_system = last_system;

    return true;
}

void ecs_pipeline_reset_iter(
    ecs_world_t *world,
    EcsPipeline *pq)
{
    ecs_pipeline_op_t *op = ecs_vector_first(pq->ops, ecs_pipeline_op_t);
    int32_t i, ran_since_merge = 0, op_index = 0, iter_count = pq->iter_count;

    /* Free state of existing iterators */
    for (i = 0; i < iter_count; i ++) {
        ecs_iter_fini(&pq->iters[i]);
    }

    if (!pq->last_system) {
        /* It's possible that all systems that were ran were removed entirely
         * from the pipeline (they could have been deleted or disabled). In that
         * case (which should be very rare) the pipeline can't make assumptions
         * about where to continue, so end frame. */
        pq->cur_i = -1;
        return;
    }

    /* Create new iterators */
    for (i = 0; i < iter_count; i ++) {
        pq->iters[i] = ecs_query_iter(world, pq->query);
    }

    /* Move iterator to last ran system */
    ecs_iter_t *it = &pq->iters[0];
    while (ecs_query_next(it)) {
        /* Progress worker iterators */
        for (i = 1; i < iter_count; i ++) {
            bool hasnext = ecs_query_next(&pq->iters[i]);
            ecs_assert_var(hasnext, ECS_INTERNAL_ERROR, NULL);
            ecs_assert(it->table == pq->iters[i].table, 
                ECS_INTERNAL_ERROR, NULL);
            ecs_assert(it->count == pq->iters[i].count,
                ECS_INTERNAL_ERROR, NULL);
        }

        if (flecs_pipeline_is_inactive(pq, it->table)) {
            continue;
        }

        for (i = 0; i < it->count; i ++) {
            ran_since_merge ++;
            if (ran_since_merge == op[op_index].count) {
                ran_since_merge = 0;
                op_index ++;
            }

            if (it->entities[i] == pq->last_system) {
                pq->cur_op = &op[op_index];
                pq->cur_i = i;
                return;
            }
        }
    }

    ecs_abort(ECS_INTERNAL_ERROR, NULL);
}

bool ecs_pipeline_update(
    ecs_world_t *world,
    ecs_entity_t pipeline,
    bool start_of_frame)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_assert(!(world->flags & EcsWorldReadonly), ECS_INVALID_OPERATION, NULL);
    ecs_assert(pipeline != 0, ECS_INTERNAL_ERROR, NULL);

    /* If any entity mutations happened that could have affected query matching
     * notify appropriate queries so caches are up to date. This includes the
     * pipeline query. */
    if (start_of_frame) {
        ecs_run_aperiodic(world, 0);
    }

    EcsPipeline *pq = ecs_get_mut(world, pipeline, EcsPipeline);
    ecs_assert(pq != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(pq->query != NULL, ECS_INTERNAL_ERROR, NULL);

    bool rebuilt = flecs_pipeline_build(world, pipeline, pq);
     if (start_of_frame) {
        /* Initialize iterators */
        int32_t i, count = pq->iter_count;
        for (i = 0; i < count; i ++) {
            pq->iters[i] = ecs_query_iter(world, pq->query);
        }
        pq->cur_op = ecs_vector_first(pq->ops, ecs_pipeline_op_t);
    } else if (rebuilt) {
        /* If pipeline got updated and we were mid frame, reset iterators */
        ecs_pipeline_reset_iter(world, pq);
        return true;
    } else {
        pq->cur_op += 1;
    }

    return false;
}

void ecs_run_pipeline(
    ecs_world_t *world,
    ecs_entity_t pipeline,
    ecs_ftime_t delta_time)
{
    ecs_assert(world != NULL, ECS_INVALID_OPERATION, NULL);

    if (!pipeline) {
        pipeline = world->pipeline;
    }

    ecs_assert(pipeline != 0, ECS_INVALID_PARAMETER, NULL);

    /* If the world is passed to ecs_run_pipeline, the function will take care
     * of staging, so the world should not be in staged mode when called. */
    if (ecs_poly_is(world, ecs_world_t)) {
        ecs_assert(!(world->flags & EcsWorldReadonly), 
            ECS_INVALID_OPERATION, NULL);

        /* Forward to worker_progress. This function handles staging, threading
         * and synchronization across workers. */
        ecs_workers_progress(world, pipeline, delta_time);
        return;

    /* If a stage is passed, the function could be ran from a worker thread. In
     * that case the main thread should manage staging, and staging should be
     * enabled. */
    } else {
        ecs_poly_assert(world, ecs_stage_t);
    }

    ecs_stage_t *stage = flecs_stage_from_world(&world);  

    EcsPipeline *pq = (EcsPipeline*)ecs_get(world, pipeline, EcsPipeline);
    ecs_assert(pq != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(pq->query != NULL, ECS_INTERNAL_ERROR, NULL);

    ecs_vector_t *ops = pq->ops;
    ecs_pipeline_op_t *op = ecs_vector_first(ops, ecs_pipeline_op_t);
    ecs_pipeline_op_t *op_last = ecs_vector_last(ops, ecs_pipeline_op_t);
    int32_t ran_since_merge = 0;

    int32_t stage_index = ecs_get_stage_id(stage->thread_ctx);
    int32_t stage_count = ecs_get_stage_count(world);

    ecs_worker_begin(stage->thread_ctx);

    ecs_iter_t *it = &pq->iters[stage_index];
    while (ecs_query_next(it)) {
        if (flecs_pipeline_is_inactive(pq, it->table)) {
            continue; /* Skip inactive systems */
        }

        EcsPoly *poly = flecs_pipeline_term_system(it);
        int32_t i, count = it->count;
        for(i = 0; i < count; i ++) {
            ecs_entity_t e = it->entities[i];
            ecs_system_t *sys = ecs_poly(poly[i].poly, ecs_system_t);

            ecs_assert(sys->entity == e, ECS_INTERNAL_ERROR, NULL);

            sys->last_frame = world->info.frame_count_total + 1;

            if (!stage_index || op->multi_threaded) {
                ecs_stage_t *s = NULL;
                if (!op->no_staging) {
                    s = stage;
                }

                ecs_run_intern(world, s, e, sys, stage_index, 
                    stage_count, delta_time, 0, 0, NULL);
            }

            ran_since_merge ++;
            world->info.systems_ran_frame ++;

            if (op != op_last && ran_since_merge == op->count) {
                ran_since_merge = 0;

                /* If the set of matched systems changed as a result of the
                 * merge, we have to reset the iterator and move it to our
                 * current position (system). If there are a lot of systems
                 * in the pipeline this can be an expensive operation, but
                 * should happen infrequently. */
                bool rebuild = ecs_worker_sync(world, pq);
                pq = (EcsPipeline*)ecs_get(world, pipeline, EcsPipeline);
                if (rebuild) {
                    i = pq->cur_i;
                    count = it->count;
                }

                poly = flecs_pipeline_term_system(it);
                op = pq->cur_op;
                op_last = ecs_vector_last(pq->ops, ecs_pipeline_op_t);
            }
        }
    }

    ecs_worker_end(stage->thread_ctx);
}

bool ecs_progress(
    ecs_world_t *world,
    ecs_ftime_t user_delta_time)
{
    float delta_time = ecs_frame_begin(world, user_delta_time);
    
    ecs_dbg_3("#[bold]progress#[reset](dt = %.2f)", (double)delta_time);
    ecs_log_push_3();
    ecs_run_pipeline(world, 0, delta_time);
    ecs_log_pop_3();

    ecs_frame_end(world);

    return !ECS_BIT_IS_SET(world->flags, EcsWorldQuit);
}

void ecs_set_time_scale(
    ecs_world_t *world,
    ecs_ftime_t scale)
{
    world->info.time_scale = scale;
}

void ecs_reset_clock(
    ecs_world_t *world)
{
    world->info.world_time_total = 0;
    world->info.world_time_total_raw = 0;
}

void ecs_set_pipeline(
    ecs_world_t *world,
    ecs_entity_t pipeline)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_check( ecs_get(world, pipeline, EcsPipeline) != NULL, 
        ECS_INVALID_PARAMETER, "not a pipeline");

    world->pipeline = pipeline;
error:
    return;
}

ecs_entity_t ecs_get_pipeline(
    const ecs_world_t *world)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    world = ecs_get_world(world);
    return world->pipeline;
error:
    return 0;
}

ecs_entity_t ecs_pipeline_init(
    ecs_world_t *world,
    const ecs_pipeline_desc_t *desc)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_assert(desc != NULL, ECS_INVALID_PARAMETER, NULL);

    ecs_entity_t result = desc->entity;
    if (!result) {
        result = ecs_new(world, 0);
    }

    ecs_query_desc_t qd = desc->query;
    if (!qd.order_by) {
        qd.order_by = flecs_entity_compare;
    }
    qd.entity = result;

    ecs_query_t *query = ecs_query_init(world, &qd);
    if (!query) {
        ecs_delete(world, result);
        return 0;
    }

    ecs_assert(query->filter.terms[0].id == EcsSystem,
        ECS_INVALID_PARAMETER, NULL);

    ecs_set(world, result, EcsPipeline, {
        .query = query,
        .match_count = -1,
        .idr_inactive = flecs_id_record_ensure(world, EcsEmpty)
    });

    return result;
}

/* -- Module implementation -- */

static
void FlecsPipelineFini(
    ecs_world_t *world,
    void *ctx)
{
    (void)ctx;
    if (ecs_get_stage_count(world)) {
        ecs_set_threads(world, 0);
    }

    ecs_assert(world->workers_running == 0, ECS_INTERNAL_ERROR, NULL);
}

#define flecs_bootstrap_phase(world, phase, depends_on)\
    flecs_bootstrap_tag(world, phase);\
    _flecs_bootstrap_phase(world, phase, depends_on)
static
void _flecs_bootstrap_phase(
    ecs_world_t *world,
    ecs_entity_t phase,
    ecs_entity_t depends_on)
{
    ecs_add_id(world, phase, EcsPhase);
    if (depends_on) {
        ecs_add_pair(world, phase, EcsDependsOn, depends_on);
    }
}

void FlecsPipelineImport(
    ecs_world_t *world)
{
    ECS_MODULE(world, FlecsPipeline);
    ECS_IMPORT(world, FlecsSystem);

    ecs_set_name_prefix(world, "Ecs");

    flecs_bootstrap_component(world, EcsPipeline);
    flecs_bootstrap_tag(world, EcsPhase);

    flecs_bootstrap_phase(world, EcsPreFrame,   0);
    flecs_bootstrap_phase(world, EcsOnLoad,     EcsPreFrame);
    flecs_bootstrap_phase(world, EcsPostLoad,   EcsOnLoad);
    flecs_bootstrap_phase(world, EcsPreUpdate,  EcsPostLoad);
    flecs_bootstrap_phase(world, EcsOnUpdate,   EcsPreUpdate);
    flecs_bootstrap_phase(world, EcsOnValidate, EcsOnUpdate);
    flecs_bootstrap_phase(world, EcsPostUpdate, EcsOnValidate);
    flecs_bootstrap_phase(world, EcsPreStore,   EcsPostUpdate);
    flecs_bootstrap_phase(world, EcsOnStore,    EcsPreStore);
    flecs_bootstrap_phase(world, EcsPostFrame,  EcsOnStore);

    ecs_set_hooks(world, EcsPipeline, {
        .ctor = ecs_default_ctor,
        .dtor = ecs_dtor(EcsPipeline)
    });

    world->pipeline = ecs_pipeline_init(world, &(ecs_pipeline_desc_t){
        .entity = ecs_entity(world, { .name = "BuiltinPipeline" }),
        .query = {
            .filter.terms = {
                { .id = EcsSystem },
                { .id = EcsPhase, .src.flags = EcsCascade, .src.trav = EcsDependsOn }
            },
            .order_by = flecs_entity_compare
        }
    });

    /* Cleanup thread administration when world is destroyed */
    ecs_atfini(world, FlecsPipelineFini, NULL);
}

#endif


#ifdef FLECS_MONITOR

ECS_COMPONENT_DECLARE(FlecsMonitor);
ECS_COMPONENT_DECLARE(EcsWorldStats);
ECS_COMPONENT_DECLARE(EcsPipelineStats);

ECS_DECLARE(EcsPeriod1s);
ECS_DECLARE(EcsPeriod1m);
ECS_DECLARE(EcsPeriod1h);
ECS_DECLARE(EcsPeriod1d);
ECS_DECLARE(EcsPeriod1w);

static int32_t flecs_day_interval_count = 24;
static int32_t flecs_week_interval_count = 168;

static
ECS_COPY(EcsPipelineStats, dst, src, {
    (void)dst;
    (void)src;
    ecs_abort(ECS_INVALID_OPERATION, "cannot copy pipeline stats component");
})

static
ECS_MOVE(EcsPipelineStats, dst, src, {
    ecs_os_memcpy_t(dst, src, EcsPipelineStats);
    ecs_os_zeromem(src);
})

static
ECS_DTOR(EcsPipelineStats, ptr, {
    ecs_pipeline_stats_fini(&ptr->stats);
})

static
void MonitorStats(ecs_iter_t *it) {
    ecs_world_t *world = it->real_world;

    EcsStatsHeader *hdr = ecs_field_w_size(it, 0, 1);
    ecs_id_t kind = ecs_pair_first(it->world, ecs_field_id(it, 1));
    void *stats = ECS_OFFSET_T(hdr, EcsStatsHeader);

    ecs_ftime_t elapsed = hdr->elapsed;
    hdr->elapsed += it->delta_time;

    int32_t t_last = (int32_t)(elapsed * 60);
    int32_t t_next = (int32_t)(hdr->elapsed * 60);
    int32_t i, dif = t_last - t_next;

    ecs_world_stats_t last_world = {0};
    ecs_pipeline_stats_t last_pipeline = {0};
    void *last = NULL;

    if (!dif) {
        /* Copy last value so we can pass it to reduce_last */
        if (kind == ecs_id(EcsWorldStats)) {
            last = &last_world;
            ecs_world_stats_copy_last(&last_world, stats);
        } else if (kind == ecs_id(EcsPipelineStats)) {
            last = &last_pipeline;
            ecs_pipeline_stats_copy_last(&last_pipeline, stats);
        }
    }

    if (kind == ecs_id(EcsWorldStats)) {
        ecs_world_stats_get(world, stats);
    } else if (kind == ecs_id(EcsPipelineStats)) {
        ecs_pipeline_stats_get(world, ecs_get_pipeline(world), stats);
    }

    if (!dif) {
        /* Still in same interval, combine with last measurement */
        hdr->reduce_count ++;
        if (kind == ecs_id(EcsWorldStats)) {
            ecs_world_stats_reduce_last(stats, last, hdr->reduce_count);
        } else if (kind == ecs_id(EcsPipelineStats)) {
            ecs_pipeline_stats_reduce_last(stats, last, hdr->reduce_count);
        }
    } else if (dif > 1) {
        /* More than 16ms has passed, backfill */
        for (i = 1; i < dif; i ++) {
            if (kind == ecs_id(EcsWorldStats)) {
                ecs_world_stats_repeat_last(stats);
            } else if (kind == ecs_id(EcsPipelineStats)) {
                ecs_world_stats_repeat_last(stats);
            }
        }
        hdr->reduce_count = 0;
    }

    if (last && kind == ecs_id(EcsPipelineStats)) {
        ecs_pipeline_stats_fini(last);
    }
}

static
void ReduceStats(ecs_iter_t *it) {
    void *dst = ecs_field_w_size(it, 0, 1);
    void *src = ecs_field_w_size(it, 0, 2);

    ecs_id_t kind = ecs_pair_first(it->world, ecs_field_id(it, 1));

    dst = ECS_OFFSET_T(dst, EcsStatsHeader);
    src = ECS_OFFSET_T(src, EcsStatsHeader);

    if (kind == ecs_id(EcsWorldStats)) {
        ecs_world_stats_reduce(dst, src);
    } else if (kind == ecs_id(EcsPipelineStats)) {
        ecs_pipeline_stats_reduce(dst, src);
    }
}

static
void AggregateStats(ecs_iter_t *it) {
    int32_t interval = *(int32_t*)it->ctx;

    EcsStatsHeader *dst_hdr = ecs_field_w_size(it, 0, 1);
    EcsStatsHeader *src_hdr = ecs_field_w_size(it, 0, 2);

    void *dst = ECS_OFFSET_T(dst_hdr, EcsStatsHeader);
    void *src = ECS_OFFSET_T(src_hdr, EcsStatsHeader);

    ecs_id_t kind = ecs_pair_first(it->world, ecs_field_id(it, 1));

    ecs_world_stats_t last_world = {0};
    ecs_pipeline_stats_t last_pipeline = {0};
    void *last = NULL;

    if (dst_hdr->reduce_count != 0) {
        /* Copy last value so we can pass it to reduce_last */
        if (kind == ecs_id(EcsWorldStats)) {
            last_world.t = 0;
            ecs_world_stats_copy_last(&last_world, dst);
            last = &last_world;
        } else if (kind == ecs_id(EcsPipelineStats)) {
            last_pipeline.t = 0;
            ecs_pipeline_stats_copy_last(&last_pipeline, dst);
            last = &last_pipeline;
        }
    }

    /* Reduce from minutes to the current day */
    if (kind == ecs_id(EcsWorldStats)) {
        ecs_world_stats_reduce(dst, src);
    } else if (kind == ecs_id(EcsPipelineStats)) {
        ecs_pipeline_stats_reduce(dst, src);
    }

    if (dst_hdr->reduce_count != 0) {
        if (kind == ecs_id(EcsWorldStats)) {
            ecs_world_stats_reduce_last(dst, last, dst_hdr->reduce_count);
        } else if (kind == ecs_id(EcsPipelineStats)) {
            ecs_pipeline_stats_reduce_last(dst, last, dst_hdr->reduce_count);
        }
    }

    /* A day has 60 24 minute intervals */
    dst_hdr->reduce_count ++;
    if (dst_hdr->reduce_count >= interval) {
        dst_hdr->reduce_count = 0;
    }

    if (last && kind == ecs_id(EcsPipelineStats)) {
        ecs_pipeline_stats_fini(last);
    }
}

static
void flecs_stats_monitor_import(
    ecs_world_t *world,
    ecs_id_t kind,
    size_t size)
{
    ecs_entity_t prev = ecs_set_scope(world, kind);

    // Called each frame, collects 60 measurements per second
    ecs_system_init(world, &(ecs_system_desc_t){
        .entity = ecs_entity(world, { .name = "Monitor1s", .add = {ecs_dependson(EcsPreFrame)} }),
        .query.filter.terms = {{
            .id = ecs_pair(kind, EcsPeriod1s),
            .src.id = EcsWorld 
        }},
        .callback = MonitorStats
    });

    // Called each second, reduces into 60 measurements per minute
    ecs_entity_t mw1m = ecs_system_init(world, &(ecs_system_desc_t){
        .entity = ecs_entity(world, { .name = "Monitor1m", .add = {ecs_dependson(EcsPreFrame)} }),
        .query.filter.terms = {{
            .id = ecs_pair(kind, EcsPeriod1m),
            .src.id = EcsWorld 
        }, {
            .id = ecs_pair(kind, EcsPeriod1s),
            .src.id = EcsWorld 
        }},
        .callback = ReduceStats,
        .interval = 1.0
    });

    // Called each minute, reduces into 60 measurements per hour
    ecs_system_init(world, &(ecs_system_desc_t){
        .entity = ecs_entity(world, { .name = "Monitor1h", .add = {ecs_dependson(EcsPreFrame)} }),
        .query.filter.terms = {{
            .id = ecs_pair(kind, EcsPeriod1h),
            .src.id = EcsWorld 
        }, {
            .id = ecs_pair(kind, EcsPeriod1m),
            .src.id = EcsWorld 
        }},
        .callback = ReduceStats,
        .rate = 60,
        .tick_source = mw1m
    });

    // Called each minute, reduces into 60 measurements per day
    ecs_system_init(world, &(ecs_system_desc_t){
        .entity = ecs_entity(world, { .name = "Monitor1d", .add = {ecs_dependson(EcsPreFrame)} }),
        .query.filter.terms = {{
            .id = ecs_pair(kind, EcsPeriod1d),
            .src.id = EcsWorld 
        }, {
            .id = ecs_pair(kind, EcsPeriod1m),
            .src.id = EcsWorld 
        }},
        .callback = AggregateStats,
        .rate = 60,
        .tick_source = mw1m,
        .ctx = &flecs_day_interval_count
    });

    // Called each hour, reduces into 60 measurements per week
    ecs_system_init(world, &(ecs_system_desc_t){
        .entity = ecs_entity(world, { .name = "Monitor1w", .add = {ecs_dependson(EcsPreFrame)} }),
        .query.filter.terms = {{
            .id = ecs_pair(kind, EcsPeriod1w),
            .src.id = EcsWorld 
        }, {
            .id = ecs_pair(kind, EcsPeriod1h),
            .src.id = EcsWorld 
        }},
        .callback = AggregateStats,
        .rate = 60,
        .tick_source = mw1m,
        .ctx = &flecs_week_interval_count
    });

    ecs_set_scope(world, prev);

    ecs_set_id(world, EcsWorld, ecs_pair(kind, EcsPeriod1s), size, NULL);
    ecs_set_id(world, EcsWorld, ecs_pair(kind, EcsPeriod1m), size, NULL);
    ecs_set_id(world, EcsWorld, ecs_pair(kind, EcsPeriod1h), size, NULL);
    ecs_set_id(world, EcsWorld, ecs_pair(kind, EcsPeriod1d), size, NULL);
    ecs_set_id(world, EcsWorld, ecs_pair(kind, EcsPeriod1w), size, NULL);
}

static
void flecs_world_monitor_import(
    ecs_world_t *world)
{
    ECS_COMPONENT_DEFINE(world, EcsWorldStats);

    flecs_stats_monitor_import(world, ecs_id(EcsWorldStats), 
        sizeof(EcsWorldStats));
}

static
void flecs_pipeline_monitor_import(
    ecs_world_t *world)
{
    ECS_COMPONENT_DEFINE(world, EcsPipelineStats);

    ecs_set_hooks(world, EcsPipelineStats, {
        .ctor = ecs_default_ctor,
        .copy = ecs_copy(EcsPipelineStats),
        .move = ecs_move(EcsPipelineStats),
        .dtor = ecs_dtor(EcsPipelineStats)
    });

    flecs_stats_monitor_import(world, ecs_id(EcsPipelineStats),
        sizeof(EcsPipelineStats));
}

void FlecsMonitorImport(
    ecs_world_t *world)
{
    ECS_MODULE_DEFINE(world, FlecsMonitor);

    ecs_set_name_prefix(world, "Ecs");

    ECS_TAG_DEFINE(world, EcsPeriod1s);
    ECS_TAG_DEFINE(world, EcsPeriod1m);
    ECS_TAG_DEFINE(world, EcsPeriod1h);
    ECS_TAG_DEFINE(world, EcsPeriod1d);
    ECS_TAG_DEFINE(world, EcsPeriod1w);

    flecs_world_monitor_import(world);
    flecs_pipeline_monitor_import(world);
    
    if (ecs_os_has_time()) {
        ecs_measure_frame_time(world, true);
        ecs_measure_system_time(world, true);
    }
}

#endif


#ifdef FLECS_TIMER

static
void AddTickSource(ecs_iter_t *it) {
    int32_t i;
    for (i = 0; i < it->count; i ++) {
        ecs_set(it->world, it->entities[i], EcsTickSource, {0});
    }
}

static
void ProgressTimers(ecs_iter_t *it) {
    EcsTimer *timer = ecs_field(it, EcsTimer, 1);
    EcsTickSource *tick_source = ecs_field(it, EcsTickSource, 2);

    ecs_assert(timer != NULL, ECS_INTERNAL_ERROR, NULL);

    int i;
    for (i = 0; i < it->count; i ++) {
        tick_source[i].tick = false;

        if (!timer[i].active) {
            continue;
        }

        const ecs_world_info_t *info = ecs_get_world_info(it->world);
        ecs_ftime_t time_elapsed = timer[i].time + info->delta_time_raw;
        ecs_ftime_t timeout = timer[i].timeout;
        
        if (time_elapsed >= timeout) {
            ecs_ftime_t t = time_elapsed - timeout;
            if (t > timeout) {
                t = 0;
            }

            timer[i].time = t; /* Initialize with remainder */            
            tick_source[i].tick = true;
            tick_source[i].time_elapsed = time_elapsed;

            if (timer[i].single_shot) {
                timer[i].active = false;
            }
        } else {
            timer[i].time = time_elapsed;
        }  
    }
}

static
void ProgressRateFilters(ecs_iter_t *it) {
    EcsRateFilter *filter = ecs_field(it, EcsRateFilter, 1);
    EcsTickSource *tick_dst = ecs_field(it, EcsTickSource, 2);

    int i;
    for (i = 0; i < it->count; i ++) {
        ecs_entity_t src = filter[i].src;
        bool inc = false;

        filter[i].time_elapsed += it->delta_time;

        if (src) {
            const EcsTickSource *tick_src = ecs_get(it->world, src, EcsTickSource);
            if (tick_src) {
                inc = tick_src->tick;
            } else {
                inc = true;
            }
        } else {
            inc = true;
        }

        if (inc) {
            filter[i].tick_count ++;
            bool triggered = !(filter[i].tick_count % filter[i].rate);
            tick_dst[i].tick = triggered;
            tick_dst[i].time_elapsed = filter[i].time_elapsed;

            if (triggered) {
                filter[i].time_elapsed = 0;
            }            
        } else {
            tick_dst[i].tick = false;
        }
    }
}

static
void ProgressTickSource(ecs_iter_t *it) {
    EcsTickSource *tick_src = ecs_field(it, EcsTickSource, 1);

    /* If tick source has no filters, tick unconditionally */
    int i;
    for (i = 0; i < it->count; i ++) {
        tick_src[i].tick = true;
        tick_src[i].time_elapsed = it->delta_time;
    }
}

ecs_entity_t ecs_set_timeout(
    ecs_world_t *world,
    ecs_entity_t timer,
    ecs_ftime_t timeout)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);

    timer = ecs_set(world, timer, EcsTimer, {
        .timeout = timeout,
        .single_shot = true,
        .active = true
    });

    ecs_system_t *system_data = ecs_poly_get(world, timer, ecs_system_t);
    if (system_data) {
        system_data->tick_source = timer;
    }

error:
    return timer;
}

ecs_ftime_t ecs_get_timeout(
    const ecs_world_t *world,
    ecs_entity_t timer)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(timer != 0, ECS_INVALID_PARAMETER, NULL);

    const EcsTimer *value = ecs_get(world, timer, EcsTimer);
    if (value) {
        return value->timeout;
    }
error:
    return 0;
}

ecs_entity_t ecs_set_interval(
    ecs_world_t *world,
    ecs_entity_t timer,
    ecs_ftime_t interval)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);

    timer = ecs_set(world, timer, EcsTimer, {
        .timeout = interval,
        .active = true
    });

    ecs_system_t *system_data = ecs_poly_get(world, timer, ecs_system_t);
    if (system_data) {
        system_data->tick_source = timer;
    }
error:
    return timer;  
}

ecs_ftime_t ecs_get_interval(
    const ecs_world_t *world,
    ecs_entity_t timer)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);

    if (!timer) {
        return 0;
    }

    const EcsTimer *value = ecs_get(world, timer, EcsTimer);
    if (value) {
        return value->timeout;
    }
error:
    return 0;
}

void ecs_start_timer(
    ecs_world_t *world,
    ecs_entity_t timer)
{
    EcsTimer *ptr = ecs_get_mut(world, timer, EcsTimer);
    ecs_check(ptr != NULL, ECS_INVALID_PARAMETER, NULL);
    ptr->active = true;
    ptr->time = 0;
error:
    return;
}

void ecs_stop_timer(
    ecs_world_t *world,
    ecs_entity_t timer)
{
    EcsTimer *ptr = ecs_get_mut(world, timer, EcsTimer);
    ecs_check(ptr != NULL, ECS_INVALID_PARAMETER, NULL);
    ptr->active = false;
error:
    return;
}

ecs_entity_t ecs_set_rate(
    ecs_world_t *world,
    ecs_entity_t filter,
    int32_t rate,
    ecs_entity_t source)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);

    filter = ecs_set(world, filter, EcsRateFilter, {
        .rate = rate,
        .src = source
    });

    ecs_system_t *system_data = ecs_poly_get(world, filter, ecs_system_t);
    if (system_data) {
        system_data->tick_source = filter;
    }  

error:
    return filter;     
}

void ecs_set_tick_source(
    ecs_world_t *world,
    ecs_entity_t system,
    ecs_entity_t tick_source)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(system != 0, ECS_INVALID_PARAMETER, NULL);
    ecs_check(tick_source != 0, ECS_INVALID_PARAMETER, NULL);

    ecs_system_t *system_data = ecs_poly_get(world, system, ecs_system_t);
    ecs_check(system_data != NULL, ECS_INVALID_PARAMETER, NULL);

    system_data->tick_source = tick_source;
error:
    return;
}

void FlecsTimerImport(
    ecs_world_t *world)
{    
    ECS_MODULE(world, FlecsTimer);

    ECS_IMPORT(world, FlecsPipeline);

    ecs_set_name_prefix(world, "Ecs");

    flecs_bootstrap_component(world, EcsTimer);
    flecs_bootstrap_component(world, EcsRateFilter);

    /* Add EcsTickSource to timers and rate filters */
    ecs_system_init(world, &(ecs_system_desc_t){
        .entity = ecs_entity(world, {.name = "AddTickSource", .add = { ecs_dependson(EcsPreFrame) }}),
        .query.filter.terms = {
            { .id = ecs_id(EcsTimer), .oper = EcsOr, .inout = EcsIn },
            { .id = ecs_id(EcsRateFilter), .oper = EcsOr, .inout = EcsIn },
            { .id = ecs_id(EcsTickSource), .oper = EcsNot, .inout = EcsOut}
        },
        .callback = AddTickSource
    });

    /* Timer handling */
    ecs_system_init(world, &(ecs_system_desc_t){
        .entity = ecs_entity(world, {.name = "ProgressTimers", .add = { ecs_dependson(EcsPreFrame)}}),
        .query.filter.terms = {
            { .id = ecs_id(EcsTimer) },
            { .id = ecs_id(EcsTickSource) }
        },
        .callback = ProgressTimers
    });

    /* Rate filter handling */
    ecs_system_init(world, &(ecs_system_desc_t){
        .entity = ecs_entity(world, {.name = "ProgressRateFilters", .add = { ecs_dependson(EcsPreFrame)}}),
        .query.filter.terms = {
            { .id = ecs_id(EcsRateFilter), .inout = EcsIn },
            { .id = ecs_id(EcsTickSource), .inout = EcsOut }
        },
        .callback = ProgressRateFilters
    });

    /* TickSource without a timer or rate filter just increases each frame */
    ecs_system_init(world, &(ecs_system_desc_t){
        .entity = ecs_entity(world, { .name = "ProgressTickSource", .add = { ecs_dependson(EcsPreFrame)}}),
        .query.filter.terms = {
            { .id = ecs_id(EcsTickSource), .inout = EcsOut },
            { .id = ecs_id(EcsRateFilter), .oper = EcsNot },
            { .id = ecs_id(EcsTimer), .oper = EcsNot }
        },
        .callback = ProgressTickSource
    });
}

#endif

#include <ctype.h>

/* Utilities for C++ API */

#ifdef FLECS_CPP

/* Convert compiler-specific typenames extracted from __PRETTY_FUNCTION__ to
 * a uniform identifier */

#define ECS_CONST_PREFIX "const "
#define ECS_STRUCT_PREFIX "struct "
#define ECS_CLASS_PREFIX "class "
#define ECS_ENUM_PREFIX "enum "

#define ECS_CONST_LEN (-1 + (ecs_size_t)sizeof(ECS_CONST_PREFIX))
#define ECS_STRUCT_LEN (-1 + (ecs_size_t)sizeof(ECS_STRUCT_PREFIX))
#define ECS_CLASS_LEN (-1 + (ecs_size_t)sizeof(ECS_CLASS_PREFIX))
#define ECS_ENUM_LEN (-1 + (ecs_size_t)sizeof(ECS_ENUM_PREFIX))

static
ecs_size_t ecs_cpp_strip_prefix(
    char *typeName,
    ecs_size_t len,
    const char *prefix,
    ecs_size_t prefix_len)
{
    if ((len > prefix_len) && !ecs_os_strncmp(typeName, prefix, prefix_len)) {
        ecs_os_memmove(typeName, typeName + prefix_len, len - prefix_len);
        typeName[len - prefix_len] = '\0';
        len -= prefix_len;
    }
    return len;
}

static 
void ecs_cpp_trim_type_name(
    char *typeName) 
{
    ecs_size_t len = ecs_os_strlen(typeName);

    len = ecs_cpp_strip_prefix(typeName, len, ECS_CONST_PREFIX, ECS_CONST_LEN);
    len = ecs_cpp_strip_prefix(typeName, len, ECS_STRUCT_PREFIX, ECS_STRUCT_LEN);
    len = ecs_cpp_strip_prefix(typeName, len, ECS_CLASS_PREFIX, ECS_CLASS_LEN);
    len = ecs_cpp_strip_prefix(typeName, len, ECS_ENUM_PREFIX, ECS_ENUM_LEN);

    while (typeName[len - 1] == ' ' ||
            typeName[len - 1] == '&' ||
            typeName[len - 1] == '*') 
    {
        len --;
        typeName[len] = '\0';
    }

    /* Remove const at end of string */
    if (len > ECS_CONST_LEN) {
        if (!ecs_os_strncmp(&typeName[len - ECS_CONST_LEN], " const", ECS_CONST_LEN)) {
            typeName[len - ECS_CONST_LEN] = '\0';
        }
        len -= ECS_CONST_LEN;
    }

    /* Check if there are any remaining "struct " strings, which can happen
     * if this is a template type on msvc. */
    if (len > ECS_STRUCT_LEN) {
        char *ptr = typeName;
        while ((ptr = strstr(ptr + 1, ECS_STRUCT_PREFIX)) != 0) {
            /* Make sure we're not matched with part of a longer identifier
             * that contains 'struct' */
            if (ptr[-1] == '<' || ptr[-1] == ',' || isspace(ptr[-1])) {
                ecs_os_memmove(ptr, ptr + ECS_STRUCT_LEN, 
                    ecs_os_strlen(ptr + ECS_STRUCT_LEN) + 1);
                len -= ECS_STRUCT_LEN;
            }
        }
    }
}

char* ecs_cpp_get_type_name(
    char *type_name, 
    const char *func_name,
    size_t len)
{
    memcpy(type_name, func_name + ECS_FUNC_NAME_FRONT(const char*, type_name), len);
    type_name[len] = '\0';
    ecs_cpp_trim_type_name(type_name);
    return type_name;
}

char* ecs_cpp_get_symbol_name(
    char *symbol_name,
    const char *type_name,
    size_t len)
{
    // Symbol is same as name, but with '::' replaced with '.'
    ecs_os_strcpy(symbol_name, type_name);

    char *ptr;
    size_t i;
    for (i = 0, ptr = symbol_name; i < len && *ptr; i ++, ptr ++) {
        if (*ptr == ':') {
            symbol_name[i] = '.';
            ptr ++;
        } else {
            symbol_name[i] = *ptr;
        }
    }

    symbol_name[i] = '\0';

    return symbol_name;
}

static
const char* cpp_func_rchr(
    const char *func_name,
    ecs_size_t func_name_len,
    char ch)
{
    const char *r = strrchr(func_name, ch);
    if ((r - func_name) >= (func_name_len - flecs_uto(ecs_size_t, ECS_FUNC_NAME_BACK))) {
        return NULL;
    }
    return r;
}

static
const char* cpp_func_max(
    const char *a,
    const char *b)
{
    if (a > b) return a;
    return b;
}

char* ecs_cpp_get_constant_name(
    char *constant_name,
    const char *func_name,
    size_t func_name_len)
{
    ecs_size_t f_len = flecs_uto(ecs_size_t, func_name_len);
    const char *start = cpp_func_rchr(func_name, f_len, ' ');
    start = cpp_func_max(start, cpp_func_rchr(func_name, f_len, ')'));
    start = cpp_func_max(start, cpp_func_rchr(func_name, f_len, ':'));
    start = cpp_func_max(start, cpp_func_rchr(func_name, f_len, ','));
    ecs_assert(start != NULL, ECS_INVALID_PARAMETER, func_name);
    start ++;
    
    ecs_size_t len = flecs_uto(ecs_size_t, 
        (f_len - (start - func_name) - flecs_uto(ecs_size_t, ECS_FUNC_NAME_BACK)));
    ecs_os_memcpy_n(constant_name, start, char, len);
    constant_name[len] = '\0';
    return constant_name;
}

// Names returned from the name_helper class do not start with ::
// but are relative to the root. If the namespace of the type
// overlaps with the namespace of the current module, strip it from
// the implicit identifier.
// This allows for registration of component types that are not in the 
// module namespace to still be registered under the module scope.
const char* ecs_cpp_trim_module(
    ecs_world_t *world,
    const char *type_name)
{
    ecs_entity_t scope = ecs_get_scope(world);
    if (!scope) {
        return type_name;
    }

    char *path = ecs_get_path_w_sep(world, 0, scope, "::", NULL);
    if (path) {
        const char *ptr = strrchr(type_name, ':');
        ecs_assert(ptr != type_name, ECS_INTERNAL_ERROR, NULL);
        if (ptr) {
            ptr --;
            ecs_assert(ptr[0] == ':', ECS_INTERNAL_ERROR, NULL);
            ecs_size_t name_path_len = (ecs_size_t)(ptr - type_name);
            if (name_path_len <= ecs_os_strlen(path)) {
                if (!ecs_os_strncmp(type_name, path, name_path_len)) {
                    type_name = &type_name[name_path_len + 2];
                }
            }
        }
    }
    ecs_os_free(path);

    return type_name;
}

// Validate registered component
void ecs_cpp_component_validate(
    ecs_world_t *world,
    ecs_entity_t id,
    const char *name,
    size_t size,
    size_t alignment,
    bool implicit_name)
{
    /* If entity has a name check if it matches */
    if (ecs_is_valid(world, id) && ecs_get_name(world, id) != NULL) {
        if (!implicit_name && id >= EcsFirstUserComponentId) {
#           ifndef FLECS_NDEBUG
            char *path = ecs_get_path_w_sep(
                world, 0, id, "::", NULL);
            if (ecs_os_strcmp(path, name)) {
                ecs_err(
                    "component '%s' already registered with name '%s'",
                    name, path);
                ecs_abort(ECS_INCONSISTENT_NAME, NULL);
            }
            ecs_os_free(path);
#           endif
        }
    } else {
        /* Ensure that the entity id valid */
        if (!ecs_is_alive(world, id)) {
            ecs_ensure(world, id);
        }

        /* Register name with entity, so that when the entity is created the
        * correct id will be resolved from the name. Only do this when the
        * entity is empty. */
        ecs_add_path_w_sep(world, id, 0, name, "::", "::");
    }

    /* If a component was already registered with this id but with a 
     * different size, the ecs_component_init function will fail. */

    /* We need to explicitly call ecs_component_init here again. Even though
     * the component was already registered, it may have been registered
     * with a different world. This ensures that the component is registered
     * with the same id for the current world. 
     * If the component was registered already, nothing will change. */
    ecs_entity_t ent = ecs_component_init(world, &(ecs_component_desc_t){
        .entity = id,
        .type.size = flecs_uto(int32_t, size),
        .type.alignment = flecs_uto(int32_t, alignment)
    });
    (void)ent;
    ecs_assert(ent == id, ECS_INTERNAL_ERROR, NULL);
}

ecs_entity_t ecs_cpp_component_register(
    ecs_world_t *world,
    ecs_entity_t id,
    const char *name,
    const char *symbol,
    ecs_size_t size,
    ecs_size_t alignment,
    bool implicit_name)
{
    (void)size;
    (void)alignment;

    /* If the component is not yet registered, ensure no other component
     * or entity has been registered with this name. Ensure component is 
     * looked up from root. */
    ecs_entity_t prev_scope = ecs_set_scope(world, 0);
    ecs_entity_t ent;
    if (id) {
        ent = id;
    } else {
        ent = ecs_lookup_path_w_sep(world, 0, name, "::", "::", false);
    }
    ecs_set_scope(world, prev_scope);

    /* If entity exists, compare symbol name to ensure that the component
     * we are trying to register under this name is the same */
    if (ent) {
        if (!id && ecs_has(world, ent, EcsComponent)) {
            const char *sym = ecs_get_symbol(world, ent);
            ecs_assert(sym != NULL, ECS_MISSING_SYMBOL, 
                ecs_get_name(world, ent));
            (void)sym;

#           ifndef FLECS_NDEBUG
            if (ecs_os_strcmp(sym, symbol)) {
                ecs_err(
                    "component with name '%s' is already registered for"\
                    " type '%s' (trying to register for type '%s')",
                        name, sym, symbol);
                ecs_abort(ECS_NAME_IN_USE, NULL);
            }
#           endif

        /* If an existing id was provided, it's possible that this id was
         * registered with another type. Make sure that in this case at
         * least the component size/alignment matches.
         * This allows applications to alias two different types to the same
         * id, which enables things like redefining a C type in C++ by
         * inheriting from it & adding utility functions etc. */
        } else {
            const EcsComponent *comp = ecs_get(world, ent, EcsComponent);
            if (comp) {
                ecs_assert(comp->size == size,
                    ECS_INVALID_COMPONENT_SIZE, NULL);
                ecs_assert(comp->alignment == alignment,
                    ECS_INVALID_COMPONENT_ALIGNMENT, NULL);
            } else {
                /* If the existing id is not a component, no checking is
                 * needed. */
            }
        }

    /* If no entity is found, lookup symbol to check if the component was
     * registered under a different name. */
    } else if (!implicit_name) {
        ent = ecs_lookup_symbol(world, symbol, false);
        ecs_assert(ent == 0, ECS_INCONSISTENT_COMPONENT_ID, symbol);
    }

    return id;
}

ecs_entity_t ecs_cpp_component_register_explicit(
    ecs_world_t *world,
    ecs_entity_t s_id,
    ecs_entity_t id,
    const char *name,
    const char *type_name,
    const char *symbol,
    size_t size,
    size_t alignment,
    bool is_component)
{
    char *existing_name = NULL;
    
    // If an explicit id is provided, it is possible that the symbol and
    // name differ from the actual type, as the application may alias
    // one type to another.
    if (!id) {
        if (!name) {
            // If no name was provided first check if a type with the provided
            // symbol was already registered.
            id = ecs_lookup_symbol(world, symbol, false);
            if (id) {
                existing_name = ecs_get_path_w_sep(world, 0, id, "::", "::");
                name = existing_name;
            } else {
                // If type is not yet known, derive from type name
                name = ecs_cpp_trim_module(world, type_name);
            }            
        }
    } else {
        // If an explicit id is provided but it has no name, inherit
        // the name from the type.
        if (!ecs_is_valid(world, id) || !ecs_get_name(world, id)) {
            name = ecs_cpp_trim_module(world, type_name);
        }
    }

    ecs_entity_t entity;
    if (is_component || size != 0) {
        entity = ecs_component_init(world, &(ecs_component_desc_t){
            .entity = ecs_entity(world, {
                .id = s_id,
                .name = name,
                .sep = "::",
                .root_sep = "::",
                .symbol = symbol
            }),
            .type.size = flecs_uto(int32_t, size),
            .type.alignment = flecs_uto(int32_t, alignment)
        });
    } else {
        entity = ecs_entity_init(world, &(ecs_entity_desc_t){
            .id = s_id,
            .name = name,
            .sep = "::",
            .root_sep = "::",
            .symbol = symbol
        });
    }

    ecs_assert(entity != 0, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(!s_id || s_id == entity, ECS_INTERNAL_ERROR, NULL);

    ecs_os_free(existing_name);

    return entity;
}

ecs_entity_t ecs_cpp_enum_constant_register(
    ecs_world_t *world,
    ecs_entity_t parent,
    ecs_entity_t id,
    const char *name,
    int value)
{
    ecs_suspend_readonly_state_t readonly_state;
    world = flecs_suspend_readonly(world, &readonly_state);

    const char *parent_name = ecs_get_name(world, parent);
    ecs_size_t parent_name_len = ecs_os_strlen(parent_name);
    if (!ecs_os_strncmp(name, parent_name, parent_name_len)) {
        name += parent_name_len;
        if (name[0] == '_') {
            name ++;
        }
    }

    ecs_entity_t prev = ecs_set_scope(world, parent);
    id = ecs_entity_init(world, &(ecs_entity_desc_t){
        .id = id,
        .name = name
    });
    ecs_assert(id != 0, ECS_INVALID_OPERATION, name);
    ecs_set_scope(world, prev);

    #ifdef FLECS_DEBUG
    const EcsComponent *cptr = ecs_get(world, parent, EcsComponent);
    ecs_assert(cptr != NULL, ECS_INVALID_PARAMETER, "enum is not a component");
    ecs_assert(cptr->size == ECS_SIZEOF(int32_t), ECS_UNSUPPORTED,
        "enum component must have 32bit size");
    #endif

    ecs_set_id(world, id, parent, sizeof(int), &value);

    flecs_resume_readonly(world, &readonly_state);

    ecs_trace("#[green]constant#[reset] %s.%s created with value %d", 
        ecs_get_name(world, parent), name, value);

    return id;
}

static int32_t flecs_reset_count = 0;

int32_t ecs_cpp_reset_count_get(void) {
    return flecs_reset_count;
}

int32_t ecs_cpp_reset_count_inc(void) {
    return ++flecs_reset_count;
}

#endif


#ifdef FLECS_DEPRECATED


#endif


#ifdef FLECS_OS_API_IMPL
#ifdef ECS_TARGET_WINDOWS
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#include <WinSock2.h>
#include <windows.h>

static
ecs_os_thread_t win_thread_new(
    ecs_os_thread_callback_t callback, 
    void *arg)
{
    HANDLE *thread = ecs_os_malloc_t(HANDLE);
    *thread = CreateThread(
        NULL, 0, (LPTHREAD_START_ROUTINE)callback, arg, 0, NULL);
    return (ecs_os_thread_t)(uintptr_t)thread;
}

static
void* win_thread_join(
    ecs_os_thread_t thr)
{
    HANDLE *thread = (HANDLE*)(uintptr_t)thr;
    DWORD r = WaitForSingleObject(*thread, INFINITE);
    if (r == WAIT_FAILED) {
        ecs_err("win_thread_join: WaitForSingleObject failed");
    }
    ecs_os_free(thread);
    return NULL;
}

static
int32_t win_ainc(
    int32_t *count) 
{
    return InterlockedIncrement(count);
}

static
int32_t win_adec(
    int32_t *count) 
{
    return InterlockedDecrement(count);
}

static
ecs_os_mutex_t win_mutex_new(void) {
    CRITICAL_SECTION *mutex = ecs_os_malloc_t(CRITICAL_SECTION);
    InitializeCriticalSection(mutex);
    return (ecs_os_mutex_t)(uintptr_t)mutex;
}

static
void win_mutex_free(
    ecs_os_mutex_t m) 
{
    CRITICAL_SECTION *mutex = (CRITICAL_SECTION*)(intptr_t)m;
    DeleteCriticalSection(mutex);
    ecs_os_free(mutex);
}

static
void win_mutex_lock(
    ecs_os_mutex_t m) 
{
    CRITICAL_SECTION *mutex = (CRITICAL_SECTION*)(intptr_t)m;
    EnterCriticalSection(mutex);
}

static
void win_mutex_unlock(
    ecs_os_mutex_t m) 
{
    CRITICAL_SECTION *mutex = (CRITICAL_SECTION*)(intptr_t)m;
    LeaveCriticalSection(mutex);
}

static
ecs_os_cond_t win_cond_new(void) {
    CONDITION_VARIABLE *cond = ecs_os_malloc_t(CONDITION_VARIABLE);
    InitializeConditionVariable(cond);
    return (ecs_os_cond_t)(uintptr_t)cond;
}

static 
void win_cond_free(
    ecs_os_cond_t c) 
{
    (void)c;
}

static 
void win_cond_signal(
    ecs_os_cond_t c) 
{
    CONDITION_VARIABLE *cond = (CONDITION_VARIABLE*)(intptr_t)c;
    WakeConditionVariable(cond);
}

static 
void win_cond_broadcast(
    ecs_os_cond_t c) 
{
    CONDITION_VARIABLE *cond = (CONDITION_VARIABLE*)(intptr_t)c;
    WakeAllConditionVariable(cond);
}

static 
void win_cond_wait(
    ecs_os_cond_t c, 
    ecs_os_mutex_t m) 
{
    CRITICAL_SECTION *mutex = (CRITICAL_SECTION*)(intptr_t)m;
    CONDITION_VARIABLE *cond = (CONDITION_VARIABLE*)(intptr_t)c;
    SleepConditionVariableCS(cond, mutex, INFINITE);
}

static bool win_time_initialized;
static double win_time_freq;
static LARGE_INTEGER win_time_start;

static
void win_time_setup(void) {
    if ( win_time_initialized) {
        return;
    }
    
    win_time_initialized = true;

    LARGE_INTEGER freq;
    QueryPerformanceFrequency(&freq);
    QueryPerformanceCounter(&win_time_start);
    win_time_freq = (double)freq.QuadPart / 1000000000.0;
}

static
void win_sleep(
    int32_t sec, 
    int32_t nanosec) 
{
    HANDLE timer;
    LARGE_INTEGER ft;

    ft.QuadPart = -((int64_t)sec * 10000000 + (int64_t)nanosec / 100);

    timer = CreateWaitableTimer(NULL, TRUE, NULL);
    SetWaitableTimer(timer, &ft, 0, NULL, NULL, 0);
    WaitForSingleObject(timer, INFINITE);
    CloseHandle(timer);
}

static double win_time_freq;
static ULONG win_current_resolution;

static
void win_enable_high_timer_resolution(bool enable)
{
    HMODULE hntdll = GetModuleHandle((LPCTSTR)"ntdll.dll");
    if (!hntdll) {
        return;
    }

    LONG (__stdcall *pNtSetTimerResolution)(
        ULONG desired, BOOLEAN set, ULONG * current);

    pNtSetTimerResolution = (LONG(__stdcall*)(ULONG, BOOLEAN, ULONG*))
        GetProcAddress(hntdll, "NtSetTimerResolution");

    if(!pNtSetTimerResolution) {
        return;
    }

    ULONG current, resolution = 10000; /* 1 ms */

    if (!enable && win_current_resolution) {
        pNtSetTimerResolution(win_current_resolution, 0, &current);
        win_current_resolution = 0;
        return;
    } else if (!enable) {
        return;
    }

    if (resolution == win_current_resolution) {
        return;
    }

    if (win_current_resolution) {
        pNtSetTimerResolution(win_current_resolution, 0, &current);
    }

    if (pNtSetTimerResolution(resolution, 1, &current)) {
        /* Try setting a lower resolution */
        resolution *= 2;
        if(pNtSetTimerResolution(resolution, 1, &current)) return;
    }

    win_current_resolution = resolution;
}

static
uint64_t win_time_now(void) {
    uint64_t now;

    LARGE_INTEGER qpc_t;
    QueryPerformanceCounter(&qpc_t);
    now = (uint64_t)(qpc_t.QuadPart / win_time_freq);

    return now;
}

static
void win_fini(void) {
    if (ecs_os_api.flags_ & EcsOsApiHighResolutionTimer) {
        win_enable_high_timer_resolution(false);
    }
}

void ecs_set_os_api_impl(void) {
    ecs_os_set_api_defaults();

    ecs_os_api_t api = ecs_os_api;

    api.thread_new_ = win_thread_new;
    api.thread_join_ = win_thread_join;
    api.ainc_ = win_ainc;
    api.adec_ = win_adec;
    api.mutex_new_ = win_mutex_new;
    api.mutex_free_ = win_mutex_free;
    api.mutex_lock_ = win_mutex_lock;
    api.mutex_unlock_ = win_mutex_unlock;
    api.cond_new_ = win_cond_new;
    api.cond_free_ = win_cond_free;
    api.cond_signal_ = win_cond_signal;
    api.cond_broadcast_ = win_cond_broadcast;
    api.cond_wait_ = win_cond_wait;
    api.sleep_ = win_sleep;
    api.now_ = win_time_now;
    api.fini_ = win_fini;

    win_time_setup();

    if (ecs_os_api.flags_ & EcsOsApiHighResolutionTimer) {
        win_enable_high_timer_resolution(true);
    }

    ecs_os_set_api(&api);
}

#else
#include "pthread.h"

#if defined(__APPLE__) && defined(__MACH__)
#include <mach/mach_time.h>
#elif defined(__EMSCRIPTEN__)
#include <emscripten.h>
#else
#include <time.h>
#endif

static
ecs_os_thread_t posix_thread_new(
    ecs_os_thread_callback_t callback, 
    void *arg)
{
    pthread_t *thread = ecs_os_malloc(sizeof(pthread_t));

    if (pthread_create (thread, NULL, callback, arg) != 0) {
        ecs_os_abort();
    }

    return (ecs_os_thread_t)(uintptr_t)thread;
}

static
void* posix_thread_join(
    ecs_os_thread_t thread)
{
    void *arg;
    pthread_t *thr = (pthread_t*)(uintptr_t)thread;
    pthread_join(*thr, &arg);
    ecs_os_free(thr);
    return arg;
}

static
int32_t posix_ainc(
    int32_t *count)
{
    int value;
#ifdef __GNUC__
    value = __sync_add_and_fetch (count, 1);
    return value;
#else
    /* Unsupported */
    abort();
#endif
}

static
int32_t posix_adec(
    int32_t *count) 
{
    int value;
#ifdef __GNUC__
    value = __sync_sub_and_fetch (count, 1);
    return value;
#else
    /* Unsupported */
    abort();
#endif
}

static
ecs_os_mutex_t posix_mutex_new(void) {
    pthread_mutex_t *mutex = ecs_os_malloc(sizeof(pthread_mutex_t));
    if (pthread_mutex_init(mutex, NULL)) {
        abort();
    }
    return (ecs_os_mutex_t)(uintptr_t)mutex;
}

static
void posix_mutex_free(
    ecs_os_mutex_t m) 
{
    pthread_mutex_t *mutex = (pthread_mutex_t*)(intptr_t)m;
    pthread_mutex_destroy(mutex);
    ecs_os_free(mutex);
}

static
void posix_mutex_lock(
    ecs_os_mutex_t m) 
{
    pthread_mutex_t *mutex = (pthread_mutex_t*)(intptr_t)m;
    if (pthread_mutex_lock(mutex)) {
        abort();
    }
}

static
void posix_mutex_unlock(
    ecs_os_mutex_t m) 
{
    pthread_mutex_t *mutex = (pthread_mutex_t*)(intptr_t)m;
    if (pthread_mutex_unlock(mutex)) {
        abort();
    }
}

static
ecs_os_cond_t posix_cond_new(void) {
    pthread_cond_t *cond = ecs_os_malloc(sizeof(pthread_cond_t));
    if (pthread_cond_init(cond, NULL)) {
        abort();
    }
    return (ecs_os_cond_t)(uintptr_t)cond;
}

static 
void posix_cond_free(
    ecs_os_cond_t c) 
{
    pthread_cond_t *cond = (pthread_cond_t*)(intptr_t)c;
    if (pthread_cond_destroy(cond)) {
        abort();
    }
    ecs_os_free(cond);
}

static 
void posix_cond_signal(
    ecs_os_cond_t c) 
{
    pthread_cond_t *cond = (pthread_cond_t*)(intptr_t)c;
    if (pthread_cond_signal(cond)) {
        abort();
    }
}

static 
void posix_cond_broadcast(
    ecs_os_cond_t c) 
{
    pthread_cond_t *cond = (pthread_cond_t*)(intptr_t)c;
    if (pthread_cond_broadcast(cond)) {
        abort();
    }
}

static 
void posix_cond_wait(
    ecs_os_cond_t c, 
    ecs_os_mutex_t m) 
{
    pthread_cond_t *cond = (pthread_cond_t*)(intptr_t)c;
    pthread_mutex_t *mutex = (pthread_mutex_t*)(intptr_t)m;
    if (pthread_cond_wait(cond, mutex)) {
        abort();
    }
}

static bool posix_time_initialized;

#if defined(__APPLE__) && defined(__MACH__)
static mach_timebase_info_data_t posix_osx_timebase;
static uint64_t posix_time_start;
#else
static uint64_t posix_time_start;
#endif

static
void posix_time_setup(void) {
    if (posix_time_initialized) {
        return;
    }
    
    posix_time_initialized = true;

    #if defined(__APPLE__) && defined(__MACH__)
        mach_timebase_info(&posix_osx_timebase);
        posix_time_start = mach_absolute_time();
    #else
        struct timespec ts;
        clock_gettime(CLOCK_MONOTONIC, &ts);
        posix_time_start = (uint64_t)ts.tv_sec*1000000000 + (uint64_t)ts.tv_nsec; 
    #endif
}

static
void posix_sleep(
    int32_t sec, 
    int32_t nanosec) 
{
    struct timespec sleepTime;
    ecs_assert(sec >= 0, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(nanosec >= 0, ECS_INTERNAL_ERROR, NULL);

    sleepTime.tv_sec = sec;
    sleepTime.tv_nsec = nanosec;
    if (nanosleep(&sleepTime, NULL)) {
        ecs_err("nanosleep failed");
    }
}

/* prevent 64-bit overflow when computing relative timestamp
    see https://gist.github.com/jspohr/3dc4f00033d79ec5bdaf67bc46c813e3
*/
#if defined(ECS_TARGET_DARWIN)
static
int64_t posix_int64_muldiv(int64_t value, int64_t numer, int64_t denom) {
    int64_t q = value / denom;
    int64_t r = value % denom;
    return q * numer + r * numer / denom;
}
#endif

static
uint64_t posix_time_now(void) {
    ecs_assert(posix_time_initialized != 0, ECS_INTERNAL_ERROR, NULL);

    uint64_t now;

    #if defined(ECS_TARGET_DARWIN)
        now = (uint64_t) posix_int64_muldiv(
            (int64_t)mach_absolute_time(), 
            (int64_t)posix_osx_timebase.numer, 
            (int64_t)posix_osx_timebase.denom);
    #elif defined(__EMSCRIPTEN__)
        now = (long long)(emscripten_get_now() * 1000.0 * 1000);
    #else
        struct timespec ts;
        clock_gettime(CLOCK_MONOTONIC, &ts);
        now = ((uint64_t)ts.tv_sec * 1000 * 1000 * 1000 + (uint64_t)ts.tv_nsec);
    #endif

    return now;
}

void ecs_set_os_api_impl(void) {
    ecs_os_set_api_defaults();

    ecs_os_api_t api = ecs_os_api;

    api.thread_new_ = posix_thread_new;
    api.thread_join_ = posix_thread_join;
    api.ainc_ = posix_ainc;
    api.adec_ = posix_adec;
    api.mutex_new_ = posix_mutex_new;
    api.mutex_free_ = posix_mutex_free;
    api.mutex_lock_ = posix_mutex_lock;
    api.mutex_unlock_ = posix_mutex_unlock;
    api.cond_new_ = posix_cond_new;
    api.cond_free_ = posix_cond_free;
    api.cond_signal_ = posix_cond_signal;
    api.cond_broadcast_ = posix_cond_broadcast;
    api.cond_wait_ = posix_cond_wait;
    api.sleep_ = posix_sleep;
    api.now_ = posix_time_now;

    posix_time_setup();

    ecs_os_set_api(&api);
}

#endif
#endif


#ifdef FLECS_PLECS

#include <stdio.h>
#include <ctype.h>

#define TOK_NEWLINE '\n'
#define TOK_WITH "with"
#define TOK_USING "using"

#define STACK_MAX_SIZE (64)

typedef struct {
    const char *name;
    const char *code;

    ecs_entity_t last_predicate;
    ecs_entity_t last_subject;
    ecs_entity_t last_object;

    ecs_id_t last_assign_id;
    ecs_entity_t assign_to;

    ecs_entity_t scope[STACK_MAX_SIZE];
    ecs_entity_t default_scope_type[STACK_MAX_SIZE];
    ecs_entity_t with[STACK_MAX_SIZE];
    ecs_entity_t using[STACK_MAX_SIZE];
    int32_t with_frames[STACK_MAX_SIZE];
    int32_t using_frames[STACK_MAX_SIZE];
    int32_t sp;
    int32_t with_frame;
    int32_t using_frame;

    char *annot[STACK_MAX_SIZE];
    int32_t annot_count;

    bool with_stmt;
    bool scope_assign_stmt;
    bool using_stmt;
    bool assign_stmt;
    bool isa_stmt;

    int32_t errors;
} plecs_state_t;

static
ecs_entity_t plecs_lookup(
    const ecs_world_t *world,
    const char *path,
    plecs_state_t *state,
    ecs_entity_t rel,
    bool is_subject)
{
    ecs_entity_t e = 0;

    if (!is_subject) {
        ecs_entity_t oneof = 0;
        if (rel) {
            if (ecs_has_id(world, rel, EcsOneOf)) {
                oneof = rel;
            } else {
                oneof = ecs_get_target(world, rel, EcsOneOf, 0);
            }
            if (oneof) {
                return ecs_lookup_path_w_sep(
                    world, oneof, path, NULL, NULL, false);
            }
        }
        int using_scope = state->using_frame - 1;
        for (; using_scope >= 0; using_scope--) {
            e = ecs_lookup_path_w_sep(
                world, state->using[using_scope], path, NULL, NULL, false);
            if (e) {
                break;
            }
        }
    }

    if (!e) {
        e = ecs_lookup_path_w_sep(world, 0, path, NULL, NULL, !is_subject);
    }

    return e;
}

/* Lookup action used for deserializing entity refs in component values */
#ifdef FLECS_EXPR
static
ecs_entity_t plecs_lookup_action(
    const ecs_world_t *world,
    const char *path,
    void *ctx)
{
    return plecs_lookup(world, path, ctx, 0, false);
}
#endif

static
ecs_entity_t plecs_ensure_entity(
    ecs_world_t *world,
    plecs_state_t *state,
    const char *path,
    ecs_entity_t rel,
    bool is_subject)
{
    if (!path) {
        return 0;
    }

    ecs_entity_t e = plecs_lookup(world, path, state, rel, is_subject);
    if (!e) {
        if (rel && flecs_get_oneof(world, rel)) {
            /* If relationship has oneof and entity was not found, don't proceed
             * with creating an entity as this can cause asserts later on */
            char *relstr = ecs_get_fullpath(world, rel);
            ecs_parser_error(state->name, 0, 0, 
                "invalid identifier '%s' for relationship '%s'", path, relstr);
            ecs_os_free(relstr);
            return 0;
        }

        if (!is_subject) {
            /* If this is not a subject create an existing empty id, which 
             * ensures that scope & with are not applied */
            e = ecs_new_id(world);
        }

        e = ecs_add_path(world, e, 0, path);
        ecs_assert(e != 0, ECS_INTERNAL_ERROR, NULL);
    } else {
        /* If entity exists, make sure it gets the right scope and with */
        if (is_subject) {
            ecs_entity_t scope = ecs_get_scope(world);
            if (scope) {
                ecs_add_pair(world, e, EcsChildOf, scope);
            }

            ecs_entity_t with = ecs_get_with(world);
            if (with) {
                ecs_add_id(world, e, with);
            }
        }
    }

    return e;
}

static
bool plecs_pred_is_subj(
    ecs_term_t *term,
    plecs_state_t *state)
{
    if (term->src.name != NULL) {
        return false;
    }
    if (term->second.name != NULL) {
        return false;
    }
    if (ecs_term_match_0(term)) {
        return false;
    }
    if (state->with_stmt) {
        return false;
    }
    if (state->assign_stmt) {
        return false;
    }
    if (state->isa_stmt) {
        return false;
    }
    if (state->using_stmt) {
        return false;
    }

    return true;
}

/* Set masks aren't useful in plecs, so translate them back to entity names */
static
const char* plecs_set_mask_to_name(
    ecs_flags32_t flags) 
{
    flags &= EcsTraverseFlags;
    if (flags == EcsSelf) {
        return "self";
    } else if (flags == EcsUp) {
        return "up";
    } else if (flags == EcsDown) {
        return "down";
    } else if (flags == EcsCascade || flags == (EcsUp|EcsCascade)) {
        return "cascade";
    } else if (flags == EcsParent) {
        return "parent";
    }
    return NULL;
}

static
char* plecs_trim_annot(
    char *annot)
{
    annot = (char*)ecs_parse_whitespace(annot);
    int32_t len = ecs_os_strlen(annot) - 1;
    while (isspace(annot[len]) && (len > 0)) {
        annot[len] = '\0';
        len --;
    }
    return annot;
}

static
void plecs_apply_annotations(
    ecs_world_t *world,
    ecs_entity_t subj,
    plecs_state_t *state)
{
    (void)world;
    (void)subj;
    (void)state;
#ifdef FLECS_DOC
    int32_t i = 0, count = state->annot_count;
    for (i = 0; i < count; i ++) {
        char *annot = state->annot[i];
        if (!ecs_os_strncmp(annot, "@brief ", 7)) {
            annot = plecs_trim_annot(annot + 7);
            ecs_doc_set_brief(world, subj, annot);
        } else if (!ecs_os_strncmp(annot, "@link ", 6)) {
            annot = plecs_trim_annot(annot + 6);
            ecs_doc_set_link(world, subj, annot);
        } else if (!ecs_os_strncmp(annot, "@name ", 6)) {
            annot = plecs_trim_annot(annot + 6);
            ecs_doc_set_name(world, subj, annot);
        } else if (!ecs_os_strncmp(annot, "@color ", 7)) {
            annot = plecs_trim_annot(annot + 7);
            ecs_doc_set_color(world, subj, annot);
        }
    }
#else
    ecs_warn("cannot apply annotations, doc addon is missing");
#endif
}

static
int plecs_create_term(
    ecs_world_t *world, 
    ecs_term_t *term,
    const char *name,
    const char *expr,
    int64_t column,
    plecs_state_t *state)
{
    state->last_subject = 0;
    state->last_predicate = 0;
    state->last_object = 0;
    state->last_assign_id = 0;

    const char *pred_name = term->first.name;
    const char *subj_name = term->src.name;
    const char *obj_name = term->second.name;

    if (!subj_name) {
        subj_name = plecs_set_mask_to_name(term->src.flags);
    }
    if (!obj_name) {
        obj_name = plecs_set_mask_to_name(term->second.flags);
    }

    if (!ecs_term_id_is_set(&term->first)) {
        ecs_parser_error(name, expr, column, "missing predicate in expression");
        return -1;
    }

    if (state->assign_stmt && !ecs_term_match_this(term)) {
        ecs_parser_error(name, expr, column, 
            "invalid statement in assign statement");
        return -1;
    }

    bool pred_as_subj = plecs_pred_is_subj(term, state);

    ecs_entity_t pred = plecs_ensure_entity(world, state, pred_name, 0, pred_as_subj); 
    ecs_entity_t subj = plecs_ensure_entity(world, state, subj_name, pred, true);
    ecs_entity_t obj = 0;

    if (ecs_term_id_is_set(&term->second)) {
        obj = plecs_ensure_entity(world, state, obj_name, pred,
            state->assign_stmt == false);
        if (!obj) {
            return -1;
        }
    }

    if (state->assign_stmt || state->isa_stmt) {
        subj = state->assign_to;
    }

    if (state->isa_stmt && obj) {
        ecs_parser_error(name, expr, column, 
            "invalid object in inheritance statement");
        return -1;
    }

    if (state->using_stmt && (obj || subj)) {
        ecs_parser_error(name, expr, column, 
            "invalid predicate/object in using statement");
        return -1;
    }

    if (state->isa_stmt) {
        pred = ecs_pair(EcsIsA, pred);
    }

    if (subj) {
        if (!obj) {
            ecs_add_id(world, subj, pred);
            state->last_assign_id = pred;
        } else {
            ecs_add_pair(world, subj, pred, obj);
            state->last_object = obj;
            state->last_assign_id = ecs_pair(pred, obj);
        }
        state->last_predicate = pred;
        state->last_subject = subj;

        pred_as_subj = false;
    } else {
        if (!obj) {
            /* If no subject or object were provided, use predicate as subj 
             * unless the expression explictly excluded the subject */
            if (pred_as_subj) {
                state->last_subject = pred;
                subj = pred;
            } else {
                state->last_predicate = pred;
                pred_as_subj = false;
            }
        } else {
            state->last_predicate = pred;
            state->last_object = obj;
            pred_as_subj = false;
        }
    }

    /* If this is a with clause (the list of entities between 'with' and scope
     * open), add subject to the array of with frames */
    if (state->with_stmt) {
        ecs_assert(pred != 0, ECS_INTERNAL_ERROR, NULL);
        ecs_id_t id;

        if (obj) {
            id = ecs_pair(pred, obj);
        } else {
            id = pred;
        }

        state->with[state->with_frame ++] = id;
    
    } else if (state->using_stmt) {
        ecs_assert(pred != 0, ECS_INTERNAL_ERROR, NULL);
        ecs_assert(obj == 0, ECS_INTERNAL_ERROR, NULL);

        state->using[state->using_frame ++] = pred;
        state->using_frames[state->sp] = state->using_frame;

    /* If this is not a with/using clause, add with frames to subject */
    } else {
        if (subj) {
            int32_t i, frame_count = state->with_frames[state->sp];
            for (i = 0; i < frame_count; i ++) {
                ecs_add_id(world, subj, state->with[i]);
            }
        }
    }

    /* If an id was provided by itself, add default scope type to it */
    ecs_entity_t default_scope_type = state->default_scope_type[state->sp];
    if (pred_as_subj && default_scope_type) {
        ecs_add_id(world, subj, default_scope_type);
    }

    /* If annotations preceded the statement, append */
    if (state->annot_count) {
        if (!subj) {
            ecs_parser_error(name, expr, column, 
                "missing subject for annotations");
            return -1;
        }

        plecs_apply_annotations(world, subj, state);
    }

    return 0;
}

static
const char* plecs_parse_inherit_stmt(
    const char *name,
    const char *expr,
    const char *ptr,
    plecs_state_t *state)
{
    if (state->isa_stmt) {
        ecs_parser_error(name, expr, ptr - expr, 
            "cannot nest inheritance");
        return NULL;
    }

    if (!state->last_subject) {
        ecs_parser_error(name, expr, ptr - expr, 
            "missing entity to assign inheritance to");
        return NULL;
    }
    
    state->isa_stmt = true;
    state->assign_to = state->last_subject;

    return ptr;
}

static
const char* plecs_parse_assign_expr(
    ecs_world_t *world,
    const char *name,
    const char *expr,
    const char *ptr,
    plecs_state_t *state) 
{
    (void)world;
    
    if (!state->assign_stmt) {
        ecs_parser_error(name, expr, ptr - expr,
            "unexpected value outside of assignment statement");
        return NULL;
    }

    ecs_id_t assign_id = state->last_assign_id;
    if (!assign_id) {
        ecs_parser_error(name, expr, ptr - expr,
            "missing type for assignment statement");
        return NULL;
    }

#ifndef FLECS_EXPR
    ecs_parser_error(name, expr, ptr - expr,
        "cannot parse value, missing FLECS_EXPR addon");
    return NULL;
#else
    ecs_entity_t assign_to = state->assign_to;
    if (!assign_to) {
        assign_to = state->last_subject;
    }

    if (!assign_to) {
        ecs_parser_error(name, expr, ptr - expr, 
            "missing entity to assign to");
        return NULL;
    }

    ecs_entity_t type = ecs_get_typeid(world, assign_id);
    if (!type) {
        char *id_str = ecs_id_str(world, assign_id);
        ecs_parser_error(name, expr, ptr - expr, 
            "invalid assignment, '%s' is not a type", id_str);
        ecs_os_free(id_str);
        return NULL;
    }

    void *value_ptr = ecs_get_mut_id(world, assign_to, assign_id);

    ptr = ecs_parse_expr(world, ptr, type, value_ptr, 
        &(ecs_parse_expr_desc_t){
            .name = name,
            .expr = expr,
            .lookup_action = plecs_lookup_action,
            .lookup_ctx = state
        });
    if (!ptr) {
        return NULL;
    }

    ecs_modified_id(world, assign_to, assign_id);
#endif

    return ptr;
}

static
const char* plecs_parse_assign_stmt(
    ecs_world_t *world,
    const char *name,
    const char *expr,
    const char *ptr,
    plecs_state_t *state) 
{
    (void)world;

    state->isa_stmt = false;

    /* Component scope (add components to entity) */
    if (!state->last_subject) {
        ecs_parser_error(name, expr, ptr - expr, 
            "missing entity to assign to");
        return NULL;
    }

    if (state->assign_stmt) {
        ecs_parser_error(name, expr, ptr - expr, 
            "invalid assign statement in assign statement");
        return NULL;
    }

    if (!state->scope_assign_stmt) {
        state->assign_to = state->last_subject;
    }

    state->assign_stmt = true;
    
    /* Assignment without a preceding component */
    if (ptr[0] == '{') {
        ecs_entity_t type = 0;

        if (state->scope_assign_stmt) {
            ecs_assert(state->assign_to == ecs_get_scope(world), 
                ECS_INTERNAL_ERROR, NULL);
        }

        /* If we're in a scope & last_subject is a type, assign to scope */
        if (ecs_get_scope(world) != 0) {
            type = ecs_get_typeid(world, state->last_subject);
            if (type != 0) {
                type = state->last_subject;
            }
        }

        /* If type hasn't been set yet, check if scope has default type */
        if (!type && !state->scope_assign_stmt) {
            type = state->default_scope_type[state->sp];
        }

        /* If no type has been found still, check if last with id is a type */
        if (!type && !state->scope_assign_stmt) {
            int32_t with_frame_count = state->with_frames[state->sp];
            if (with_frame_count) {
                type = state->with[with_frame_count - 1];
            }
        }

        if (!type) {
            ecs_parser_error(name, expr, ptr - expr, 
                "missing type for assignment");
            return NULL;
        }

        state->last_assign_id = type;
    }

    return ptr;
}

static
const char* plecs_parse_using_stmt(
    const char *name,
    const char *expr,
    const char *ptr,
    plecs_state_t *state) 
{
    if (state->isa_stmt || state->assign_stmt) {
        ecs_parser_error(name, expr, ptr - expr, 
            "invalid usage of using keyword");
        return NULL;
    }

    /* Add following expressions to using list */
    state->using_stmt = true;

    return ptr + 5;
}

static
const char* plecs_parse_with_stmt(
    const char *name,
    const char *expr,
    const char *ptr,
    plecs_state_t *state) 
{
    if (state->isa_stmt) {
        ecs_parser_error(name, expr, ptr - expr, 
            "invalid with after inheritance");
        return NULL;
    }

    if (state->assign_stmt) {
        ecs_parser_error(name, expr, ptr - expr, 
            "invalid with in assign_stmt");
        return NULL;
    }

    /* Add following expressions to with list */
    state->with_stmt = true;
    return ptr + 5;
}

static
const char* plecs_parse_scope_open(
    ecs_world_t *world,
    const char *name,
    const char *expr,
    const char *ptr,
    plecs_state_t *state) 
{
    state->isa_stmt = false;

    if (state->assign_stmt) {
        ecs_parser_error(name, expr, ptr - expr, 
            "invalid scope in assign_stmt");
        return NULL;
    }

    state->sp ++;

    ecs_entity_t scope = 0;
    ecs_entity_t default_scope_type = 0;

    if (!state->with_stmt) {
        if (state->last_subject) {
            scope = state->last_subject;
            ecs_set_scope(world, state->last_subject);

            /* Check if scope has a default child component */
            ecs_entity_t def_type_src = ecs_get_target_for_id(world, scope, 
                0, ecs_pair(EcsDefaultChildComponent, EcsWildcard));

            if (def_type_src) {
                default_scope_type = ecs_get_target(
                    world, def_type_src, EcsDefaultChildComponent, 0);
            }
        } else {
            if (state->last_object) {
                scope = ecs_pair(
                    state->last_predicate, state->last_object);
                ecs_set_with(world, scope);
            } else {
                if (state->last_predicate) {
                    scope = ecs_pair(EcsChildOf, state->last_predicate);
                }
                ecs_set_scope(world, state->last_predicate);
            }
        }

        state->scope[state->sp] = scope;
        state->default_scope_type[state->sp] = default_scope_type;
    } else {
        state->scope[state->sp] = state->scope[state->sp - 1];
        state->default_scope_type[state->sp] = 
            state->default_scope_type[state->sp - 1];
    }

    state->using_frames[state->sp] = state->using_frame;
    state->with_frames[state->sp] = state->with_frame;
    state->with_stmt = false;

    return ptr;
}

static
const char* plecs_parse_scope_close(
    ecs_world_t *world,
    const char *name,
    const char *expr,
    const char *ptr,
    plecs_state_t *state) 
{
    if (state->isa_stmt) {
        ecs_parser_error(name, expr, ptr - expr, 
            "invalid '}' after inheritance statement");
        return NULL;
    }

    if (state->assign_stmt) {
        ecs_parser_error(name, expr, ptr - expr, 
            "unfinished assignment before }");
        return NULL;
    }

    state->scope[state->sp] = 0;
    state->default_scope_type[state->sp] = 0;
    state->sp --;

    if (state->sp < 0) {
        ecs_parser_error(name, expr, ptr - expr, "invalid } without a {");
        return NULL;
    }

    ecs_id_t id = state->scope[state->sp];

    if (!id || ECS_HAS_ROLE(id, PAIR)) {
        ecs_set_with(world, id);
    }

    if (!id || !ECS_HAS_ROLE(id, PAIR)) {
        ecs_set_scope(world, id);
    }

    state->with_frame = state->with_frames[state->sp];
    state->using_frame = state->using_frames[state->sp];
    state->last_subject = 0;
    state->assign_stmt = false;

    return ptr;
}

static
const char *plecs_parse_plecs_term(
    ecs_world_t *world,
    const char *name,
    const char *expr,
    const char *ptr,
    plecs_state_t *state)
{
    ecs_term_t term = {0};
    ecs_entity_t scope = ecs_get_scope(world);

    /* If first character is a (, this should be interpreted as an id assigned
     * to the current scope if:
     * - this is not already an assignment: "Foo = (Hello, World)" 
     * - this is in a scope
     */
    bool scope_assignment = (ptr[0] == '(') && !state->assign_stmt && scope != 0;

    ptr = ecs_parse_term(world, name, expr, ptr, &term);
    if (!ptr) {
        return NULL;
    }

    if (!ecs_term_is_initialized(&term)) {
        ecs_parser_error(name, expr, ptr - expr, "expected identifier");
        return NULL; /* No term found */
    }

    /* Lookahead to check if this is an implicit scope assignment (no parens) */
    if (ptr[0] == '=') {
        const char *tptr = ecs_parse_fluff(ptr + 1, NULL);
        if (tptr[0] == '{') {
            ecs_entity_t pred = plecs_lookup(
                world, term.first.name, state, 0, false);
            ecs_entity_t obj = plecs_lookup(
                world, term.second.name, state, pred, false);
            ecs_id_t id = 0;
            if (pred && obj) {
                id = ecs_pair(pred, obj);
            } else if (pred) {
                id = pred;
            }

            if (id && (ecs_get_typeid(world, id) != 0)) {
                scope_assignment = true;
            }
        } 
    }

    bool prev = state->assign_stmt;
    if (scope_assignment) {
        state->assign_stmt = true;
        state->assign_to = scope;
    }
    if (plecs_create_term(world, &term, name, expr, (ptr - expr), state)) {
        ecs_term_fini(&term);
        return NULL; /* Failed to create term */
    }
    if (scope_assignment) {
        state->last_subject = state->last_assign_id;
        state->scope_assign_stmt = true;
    }
    state->assign_stmt = prev;

    ecs_term_fini(&term);

    return ptr;
}

static
const char* plecs_parse_annotation(
    const char *name,
    const char *expr,
    const char *ptr,
    plecs_state_t *state)
{
    do {
        if(state->annot_count >= STACK_MAX_SIZE) {
            ecs_parser_error(name, expr, ptr - expr, 
                "max number of annotations reached");
            return NULL;
        }

        char ch;
        const char *start = ptr;
        for (; (ch = *ptr) && ch != '\n'; ptr ++) { }

        int32_t len = (int32_t)(ptr - start);
        char *annot = ecs_os_malloc_n(char, len + 1);
        ecs_os_memcpy_n(annot, start, char, len);
        annot[len] = '\0';

        state->annot[state->annot_count] = annot;
        state->annot_count ++;

        ptr = ecs_parse_fluff(ptr, NULL);
    } while (ptr[0] == '@');

    return ptr;
}

static
void plecs_clear_annotations(
    plecs_state_t *state)
{
    int32_t i, count = state->annot_count;
    for (i = 0; i < count; i ++) {
        ecs_os_free(state->annot[i]);
    }
    state->annot_count = 0;
}

static
const char* plecs_parse_stmt(
    ecs_world_t *world,
    const char *name,
    const char *expr,
    const char *ptr,
    plecs_state_t *state)
{
    state->assign_stmt = false;
    state->scope_assign_stmt = false;
    state->isa_stmt = false;
    state->with_stmt = false;
    state->using_stmt = false;
    state->last_subject = 0;
    state->last_predicate = 0;
    state->last_object = 0;

    plecs_clear_annotations(state);

    ptr = ecs_parse_fluff(ptr, NULL);

    char ch = ptr[0];

    if (!ch) {
        goto done;
    } else if (ch == '{') {
        ptr = ecs_parse_fluff(ptr + 1, NULL);
        goto scope_open;
    } else if (ch == '}') {
        ptr = ecs_parse_fluff(ptr + 1, NULL);
        goto scope_close;
    } else if (ch == '(') {
        goto term_expr;
    } else if (ch == '@') {
        ptr = plecs_parse_annotation(name, expr, ptr, state);
        if (!ptr) goto error;
        goto term_expr;
    } else if (!ecs_os_strncmp(ptr, TOK_USING " ", 5)) {
        ptr = plecs_parse_using_stmt(name, expr, ptr, state);
        if (!ptr) goto error;
        goto term_expr;
    } else if (!ecs_os_strncmp(ptr, TOK_WITH " ", 5)) {
        ptr = plecs_parse_with_stmt(name, expr, ptr, state);
        if (!ptr) goto error;
        goto term_expr;
    } else {
        goto term_expr;
    }

term_expr:
    if (!ptr[0]) {
        goto done;
    }

    if (!(ptr = plecs_parse_plecs_term(world, name, ptr, ptr, state))) {
        goto error;
    }

    ptr = ecs_parse_fluff(ptr, NULL);

    if (ptr[0] == '{' && !isspace(ptr[-1])) {
        /* A '{' directly after an identifier (no whitespace) is a literal */
        goto assign_expr;
    }

    if (!state->using_stmt) {
        if (ptr[0] == ':') {
            ptr = ecs_parse_fluff(ptr + 1, NULL);
            goto inherit_stmt;
        } else if (ptr[0] == '=') {
            ptr = ecs_parse_fluff(ptr + 1, NULL);
            goto assign_stmt;
        } else if (ptr[0] == ',') {
            ptr = ecs_parse_fluff(ptr + 1, NULL);
            goto term_expr;
        } else if (ptr[0] == '{') {
            state->assign_stmt = false;
            ptr = ecs_parse_fluff(ptr + 1, NULL);
            goto scope_open;
        }
    }

    state->assign_stmt = false;
    goto done;

inherit_stmt:
    ptr = plecs_parse_inherit_stmt(name, expr, ptr, state);
    if (!ptr) goto error;

    /* Expect base identifier */
    goto term_expr;

assign_stmt:
    ptr = plecs_parse_assign_stmt(world, name, expr, ptr, state);
    if (!ptr) goto error;

    ptr = ecs_parse_fluff(ptr, NULL);

    /* Assignment without a preceding component */
    if (ptr[0] == '{') {
        goto assign_expr;
    }

    /* Expect component identifiers */
    goto term_expr;

assign_expr:
    ptr = plecs_parse_assign_expr(world, name, expr, ptr, state);
    if (!ptr) goto error;

    ptr = ecs_parse_fluff(ptr, NULL);
    if (ptr[0] == ',') {
        ptr ++;
        goto term_expr;
    } else if (ptr[0] == '{') {
        state->assign_stmt = false;
        ptr ++;
        goto scope_open;
    } else {
        state->assign_stmt = false;
        goto done;
    }

scope_open:
    ptr = plecs_parse_scope_open(world, name, expr, ptr, state);
    if (!ptr) goto error;
    goto done;

scope_close:
    ptr = plecs_parse_scope_close(world, name, expr, ptr, state);
    if (!ptr) goto error;
    goto done;

done:
    return ptr;
error:
    return NULL;
}

int ecs_plecs_from_str(
    ecs_world_t *world,
    const char *name,
    const char *expr) 
{
    const char *ptr = expr;
    ecs_term_t term = {0};
    plecs_state_t state = {0};

    if (!expr) {
        return 0;
    }

    state.scope[0] = 0;
    ecs_entity_t prev_scope = ecs_set_scope(world, 0);
    ecs_entity_t prev_with = ecs_set_with(world, 0);

    do {
        expr = ptr = plecs_parse_stmt(world, name, expr, ptr, &state);
        if (!ptr) {
            goto error;
        }

        if (!ptr[0]) {
            break; /* End of expression */
        }
    } while (true);

    ecs_set_scope(world, prev_scope);
    ecs_set_with(world, prev_with);

    plecs_clear_annotations(&state);

    if (state.sp != 0) {
        ecs_parser_error(name, expr, 0, "missing end of scope");
        goto error;
    }

    if (state.assign_stmt) {
        ecs_parser_error(name, expr, 0, "unfinished assignment");
        goto error;
    }

    if (state.errors) {
        goto error;
    }

    return 0;
error:
    ecs_set_scope(world, state.scope[0]);
    ecs_set_with(world, prev_with);
    ecs_term_fini(&term);
    return -1;
}

int ecs_plecs_from_file(
    ecs_world_t *world,
    const char *filename) 
{
    FILE* file;
    char* content = NULL;
    int32_t bytes;
    size_t size;

    /* Open file for reading */
    ecs_os_fopen(&file, filename, "r");
    if (!file) {
        ecs_err("%s (%s)", ecs_os_strerror(errno), filename);
        goto error;
    }

    /* Determine file size */
    fseek(file, 0 , SEEK_END);
    bytes = (int32_t)ftell(file);
    if (bytes == -1) {
        goto error;
    }
    rewind(file);

    /* Load contents in memory */
    content = ecs_os_malloc(bytes + 1);
    size = (size_t)bytes;
    if (!(size = fread(content, 1, size, file)) && bytes) {
        ecs_err("%s: read zero bytes instead of %d", filename, size);
        ecs_os_free(content);
        content = NULL;
        goto error;
    } else {
        content[size] = '\0';
    }

    fclose(file);

    int result = ecs_plecs_from_str(world, filename, content);
    ecs_os_free(content);
    return result;
error:
    ecs_os_free(content);
    return -1;
}

#endif


#ifdef FLECS_RULES

#include <stdio.h>

#define ECS_RULE_MAX_VAR_COUNT (32)

#define RULE_PAIR_PREDICATE (1)
#define RULE_PAIR_OBJECT (2)

/* A rule pair contains a predicate and object that can be stored in a register. */
typedef struct ecs_rule_pair_t {
    union {
        int32_t reg;
        ecs_entity_t id;
    } first;
    union {
        int32_t reg;
        ecs_entity_t id;
    } second;
    int32_t reg_mask; /* bit 1 = predicate, bit 2 = object */

    bool transitive; /* Is predicate transitive */
    bool final;      /* Is predicate final */
    bool reflexive;  /* Is predicate reflexive */
    bool acyclic;    /* Is predicate acyclic */
    bool second_0;
} ecs_rule_pair_t;

/* Filter for evaluating & reifing types and variables. Filters are created ad-
 * hoc from pairs, and take into account all variables that had been resolved
 * up to that point. */
typedef struct ecs_rule_filter_t {
    ecs_id_t mask;  /* Mask with wildcard in place of variables */

    bool wildcard; /* Does the filter contain wildcards */
    bool first_wildcard; /* Is predicate a wildcard */
    bool second_wildcard; /* Is object a wildcard */
    bool same_var; /* True if first & second are both the same variable */

    int32_t hi_var; /* If hi part should be stored in var, this is the var id */
    int32_t lo_var; /* If lo part should be stored in var, this is the var id */
} ecs_rule_filter_t;

/* A rule register stores temporary values for rule variables */
typedef enum ecs_rule_var_kind_t {
    EcsRuleVarKindTable, /* Used for sorting, must be smallest */
    EcsRuleVarKindEntity,
    EcsRuleVarKindUnknown
} ecs_rule_var_kind_t;

/* Operations describe how the rule should be evaluated */
typedef enum ecs_rule_op_kind_t {
    EcsRuleInput,       /* Input placeholder, first instruction in every rule */
    EcsRuleSelect,      /* Selects all ables for a given predicate */
    EcsRuleWith,        /* Applies a filter to a table or entity */
    EcsRuleSubSet,      /* Finds all subsets for transitive relationship */
    EcsRuleSuperSet,    /* Finds all supersets for a transitive relationship */
    EcsRuleStore,       /* Store entity in table or entity variable */
    EcsRuleEach,        /* Forwards each entity in a table */
    EcsRuleSetJmp,      /* Set label for jump operation to one of two values */
    EcsRuleJump,        /* Jump to an operation label */
    EcsRuleNot,         /* Invert result of an operation */
    EcsRuleInTable,     /* Test if entity (subject) is in table (r_in) */
    EcsRuleEq,          /* Test if entity in (subject) and (r_in) are equal */
    EcsRuleYield        /* Yield result */
} ecs_rule_op_kind_t;

/* Single operation */
typedef struct ecs_rule_op_t {
    ecs_rule_op_kind_t kind;    /* What kind of operation is it */
    ecs_rule_pair_t filter;     /* Parameter that contains optional filter */
    ecs_entity_t subject;       /* If set, operation has a constant subject */

    int32_t on_pass;            /* Jump location when match succeeds */
    int32_t on_fail;            /* Jump location when match fails */
    int32_t frame;              /* Register frame */

    int32_t term;               /* Corresponding term index in signature */
    int32_t r_in;               /* Optional In/Out registers */
    int32_t r_out;

    bool has_in, has_out;       /* Keep track of whether operation uses input
                                 * and/or output registers. This helps with
                                 * debugging rule programs. */
} ecs_rule_op_t;

/* With context. Shared with select. */
typedef struct ecs_rule_with_ctx_t {
    ecs_id_record_t *idr;      /* Currently evaluated table set */
    ecs_table_cache_iter_t it;
    int32_t column;
} ecs_rule_with_ctx_t;

/* Subset context */
typedef struct ecs_rule_subset_frame_t {
    ecs_rule_with_ctx_t with_ctx;
    ecs_table_t *table;
    int32_t row;
    int32_t column;
} ecs_rule_subset_frame_t;

typedef struct ecs_rule_subset_ctx_t {
    ecs_rule_subset_frame_t storage[16]; /* Alloc-free array for small trees */
    ecs_rule_subset_frame_t *stack;
    int32_t sp;
} ecs_rule_subset_ctx_t;

/* Superset context */
typedef struct ecs_rule_superset_frame_t {
    ecs_table_t *table;
    int32_t column;
} ecs_rule_superset_frame_t;

typedef struct ecs_rule_superset_ctx_t {
    ecs_rule_superset_frame_t storage[16]; /* Alloc-free array for small trees */
    ecs_rule_superset_frame_t *stack;
    ecs_id_record_t *idr;
    int32_t sp;
} ecs_rule_superset_ctx_t;

/* Each context */
typedef struct ecs_rule_each_ctx_t {
    int32_t row;                /* Currently evaluated row in evaluated table */
} ecs_rule_each_ctx_t;

/* Jump context */
typedef struct ecs_rule_setjmp_ctx_t {
    int32_t label;             /* Operation label to jump to */
} ecs_rule_setjmp_ctx_t;

/* Operation context. This is a per-operation, per-iterator structure that
 * stores information for stateful operations. */
typedef struct ecs_rule_op_ctx_t {
    union {
        ecs_rule_subset_ctx_t subset;
        ecs_rule_superset_ctx_t superset;
        ecs_rule_with_ctx_t with;
        ecs_rule_each_ctx_t each;
        ecs_rule_setjmp_ctx_t setjmp;
    } is;
} ecs_rule_op_ctx_t;

/* Rule variables allow for the rule to be parameterized */
typedef struct ecs_rule_var_t {
    ecs_rule_var_kind_t kind;
    char *name;       /* Variable name */
    int32_t id;       /* Unique variable id */
    int32_t other;    /* Id to table variable (-1 if none exists) */
    int32_t occurs;   /* Number of occurrences (used for operation ordering) */
    int32_t depth;  /* Depth in dependency tree (used for operation ordering) */
    bool marked;      /* Used for cycle detection */
} ecs_rule_var_t;

/* Variable ids per term */
typedef struct ecs_rule_term_vars_t {
    int32_t first;
    int32_t src;
    int32_t second;
} ecs_rule_term_vars_t;

/* Top-level rule datastructure */
struct ecs_rule_t {
    ecs_header_t hdr;
    
    ecs_world_t *world;         /* Ref to world so rule can be used by itself */
    ecs_rule_op_t *operations;  /* Operations array */
    ecs_filter_t filter;        /* Filter of rule */

    /* Variable array */
    ecs_rule_var_t vars[ECS_RULE_MAX_VAR_COUNT];

    /* Passed to iterator */
    char *var_names[ECS_RULE_MAX_VAR_COUNT]; 

    /* Variable ids used in terms */
    ecs_rule_term_vars_t term_vars[ECS_RULE_MAX_VAR_COUNT];

    /* Variable evaluation order */
    int32_t var_eval_order[ECS_RULE_MAX_VAR_COUNT];

    int32_t var_count;          /* Number of variables in signature */
    int32_t subj_var_count;
    int32_t frame_count;        /* Number of register frames */
    int32_t operation_count;    /* Number of operations in rule */

    ecs_iterable_t iterable;    /* Iterable mixin */
};

/* ecs_rule_t mixins */
ecs_mixins_t ecs_rule_t_mixins = {
    .type_name = "ecs_rule_t",
    .elems = {
        [EcsMixinWorld] = offsetof(ecs_rule_t, world),
        [EcsMixinIterable] = offsetof(ecs_rule_t, iterable)
    }
};

static
void rule_error(
    const ecs_rule_t *rule,
    const char *fmt,
    ...)
{
    char *fstr = ecs_filter_str(rule->world, &rule->filter);
    va_list valist;
    va_start(valist, fmt);
    ecs_parser_errorv(rule->filter.name, fstr, -1, fmt, valist);
    va_end(valist);
    ecs_os_free(fstr);
}

static
bool subj_is_set(
    ecs_term_t *term)
{
    return ecs_term_id_is_set(&term->src);
}

static
bool obj_is_set(
    ecs_term_t *term)
{
    return ecs_term_id_is_set(&term->second) || term->role == ECS_PAIR;
}

static
ecs_rule_op_t* create_operation(
    ecs_rule_t *rule)
{
    int32_t cur = rule->operation_count ++;
    rule->operations = ecs_os_realloc(
        rule->operations, (cur + 1) * ECS_SIZEOF(ecs_rule_op_t));

    ecs_rule_op_t *result = &rule->operations[cur];
    ecs_os_memset_t(result, 0, ecs_rule_op_t);

    return result;
}

static
const char* get_var_name(const char *name) {
    if (name && !ecs_os_strcmp(name, "This")) {
        /* Make sure that both This and . resolve to the same variable */
        name = ".";
    }

    return name;
}

static
ecs_rule_var_t* create_variable(
    ecs_rule_t *rule,
    ecs_rule_var_kind_t kind,
    const char *name)
{
    int32_t cur = ++ rule->var_count;
    
    name = get_var_name(name);
    if (name && !ecs_os_strcmp(name, "*")) {
        /* Wildcards are treated as anonymous variables */
        name = NULL;
    }

    ecs_rule_var_t *var = &rule->vars[cur - 1];
    if (name) {
        var->name = ecs_os_strdup(name);
    } else {
        /* Anonymous register */
        char name_buff[32];
        ecs_os_sprintf(name_buff, "_%u", cur - 1);
        var->name = ecs_os_strdup(name_buff);
    }

    var->kind = kind;

    /* The variable id is the location in the variable array and also points to
     * the register element that corresponds with the variable. */
    var->id = cur - 1;

    /* Depth is used to calculate how far the variable is from the root, where
     * the root is the variable with 0 dependencies. */
    var->depth = UINT8_MAX;
    var->marked = false;
    var->occurs = 0;

    return var;
}

static
ecs_rule_var_t* create_anonymous_variable(
    ecs_rule_t *rule,
    ecs_rule_var_kind_t kind)
{
    return create_variable(rule, kind, NULL);
}

/* Find variable with specified name and type. If Unknown is provided as type,
 * the function will return any variable with the provided name. The root 
 * variable can occur both as a table and entity variable, as some rules
 * require that each entity in a table is iterated. In this case, there are two
 * variables, one for the table and one for the entities in the table, that both
 * have the same name. */
static
ecs_rule_var_t* find_variable(
    const ecs_rule_t *rule,
    ecs_rule_var_kind_t kind,
    const char *name)
{
    ecs_assert(rule != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(name != NULL, ECS_INTERNAL_ERROR, NULL);

    name = get_var_name(name);

    const ecs_rule_var_t *variables = rule->vars;
    int32_t i, count = rule->var_count;
    
    for (i = 0; i < count; i ++) {
        const ecs_rule_var_t *variable = &variables[i];
        if (!ecs_os_strcmp(name, variable->name)) {
            if (kind == EcsRuleVarKindUnknown || kind == variable->kind) {
                return (ecs_rule_var_t*)variable;
            }
        }
    }

    return NULL;
}

/* Ensure variable with specified name and type exists. If an existing variable
 * is found with an unknown type, its type will be overwritten with the 
 * specified type. During the variable ordering phase it is not yet clear which
 * variable is the root. Which variable is the root determines its type, which
 * is why during this phase variables are still untyped. */
static
ecs_rule_var_t* ensure_variable(
    ecs_rule_t *rule,
    ecs_rule_var_kind_t kind,
    const char *name)
{
    ecs_rule_var_t *var = find_variable(rule, kind, name);
    if (!var) {
        var = create_variable(rule, kind, name);
    } else {
        if (var->kind == EcsRuleVarKindUnknown) {
            var->kind = kind;
        }
    }

    return var;
}

static
const char *term_id_var_name(
    ecs_term_id_t *term_id)
{
    if (term_id->flags & EcsIsVariable) {
        if (term_id->name) {
            return term_id->name;
        } else if (term_id->id == EcsThis) {
            return ".";
        } else if (term_id->id == EcsWildcard) {
            return "*";
        } else if (term_id->id == EcsAny) {
            return "_";
        } else if (term_id->id == EcsVariable) {
            return "$";
        } else {
            ecs_check(term_id->name != NULL, ECS_INVALID_PARAMETER, NULL);
        }
    }

error:
    return NULL;
}

static
int ensure_term_id_variable(
    ecs_rule_t *rule,
    ecs_term_t *term,
    ecs_term_id_t *term_id)
{
    if (term_id->flags & EcsIsVariable) {
        if (term_id->id == EcsAny) {
            /* Any variables aren't translated to rule variables since their
             * result isn't stored. */
            return 0;
        }

        const char *name = term_id_var_name(term_id);

        /* If this is a Not term, it should not introduce new variables. It may
         * however create entity variables if there already was an existing
         * table variable */
        if (term->oper == EcsNot) {
            if (!find_variable(rule, EcsRuleVarKindUnknown, name)) {
                rule_error(rule, "variable in '%s' only appears in Not term",
                    name);
                return -1;
            }
        }

        ecs_rule_var_t *var = ensure_variable(rule, EcsRuleVarKindEntity, name);
        ecs_os_strset(&term_id->name, var->name);
        return 0;
    }

    return 0;
}

static
bool term_id_is_variable(
    ecs_term_id_t *term_id)
{
    return term_id->flags & EcsIsVariable;
}

/* Get variable from a term identifier */
static
ecs_rule_var_t* term_id_to_var(
    ecs_rule_t *rule,
    ecs_term_id_t *id)
{
    if (id->flags & EcsIsVariable) {
        return find_variable(rule, EcsRuleVarKindUnknown, term_id_var_name(id));
    }
    return NULL;
}

/* Get variable from a term predicate */
static
ecs_rule_var_t* term_pred(
    ecs_rule_t *rule,
    ecs_term_t *term)
{
    return term_id_to_var(rule, &term->first);
}

/* Get variable from a term subject */
static
ecs_rule_var_t* term_subj(
    ecs_rule_t *rule,
    ecs_term_t *term)
{
    return term_id_to_var(rule, &term->src);
}

/* Get variable from a term object */
static
ecs_rule_var_t* term_obj(
    ecs_rule_t *rule,
    ecs_term_t *term)
{
    if (obj_is_set(term)) {
        return term_id_to_var(rule, &term->second);
    } else {
        return NULL;
    }
}

/* Return predicate variable from pair */
static
ecs_rule_var_t* pair_pred(
    ecs_rule_t *rule,
    const ecs_rule_pair_t *pair)
{
    if (pair->reg_mask & RULE_PAIR_PREDICATE) {
        return &rule->vars[pair->first.reg];
    } else {
        return NULL;
    }
}

/* Return object variable from pair */
static
ecs_rule_var_t* pair_obj(
    ecs_rule_t *rule,
    const ecs_rule_pair_t *pair)
{
    if (pair->reg_mask & RULE_PAIR_OBJECT) {
        return &rule->vars[pair->second.reg];
    } else {
        return NULL;
    }
}

/* Create new frame for storing register values. Each operation that yields data
 * gets its own register frame, which contains all variables reified up to that
 * point. The preceding frame always contains the reified variables from the
 * previous operation. Operations that do not yield data (such as control flow)
 * do not have their own frames. */
static
int32_t push_frame(
    ecs_rule_t *rule)
{
    return rule->frame_count ++;
}

/* Get register array for current stack frame. The stack frame is determined by
 * the current operation that is evaluated. The register array contains the
 * values for the reified variables. If a variable hasn't been reified yet, its
 * register will store a wildcard. */
static
ecs_var_t* get_register_frame(
    const ecs_rule_iter_t *it,
    int32_t frame)    
{
    if (it->registers) {
        return &it->registers[frame * it->rule->var_count];
    } else {
        return NULL;
    }
}

/* Get register array for current stack frame. The stack frame is determined by
 * the current operation that is evaluated. The register array contains the
 * values for the reified variables. If a variable hasn't been reified yet, its
 * register will store a wildcard. */
static
ecs_var_t* get_registers(
    const ecs_rule_iter_t *it,
    ecs_rule_op_t *op)    
{
    return get_register_frame(it, op->frame);
}

/* Get columns array. Columns store, for each matched column in a table, the 
 * index at which it occurs. This reduces the amount of searching that
 * operations need to do in a type, since select/with already provide it. */
static
int32_t* rule_get_columns_frame(
    ecs_rule_iter_t *it,
    int32_t frame)    
{
    return &it->columns[frame * it->rule->filter.term_count];
}

static
int32_t* rule_get_columns(
    ecs_rule_iter_t *it,
    ecs_rule_op_t *op)    
{
    return rule_get_columns_frame(it, op->frame);
}

static
void entity_reg_set(
    const ecs_rule_t *rule,
    ecs_var_t *regs,
    int32_t r,
    ecs_entity_t entity)
{
    (void)rule;
    ecs_assert(rule->vars[r].kind == EcsRuleVarKindEntity, 
        ECS_INTERNAL_ERROR, NULL);
    ecs_check(ecs_is_valid(rule->world, entity), ECS_INVALID_PARAMETER, NULL);
    regs[r].entity = entity;
error:
    return;
}

static
ecs_entity_t entity_reg_get(
    const ecs_rule_t *rule,
    ecs_var_t *regs,
    int32_t r)
{
    (void)rule;
    ecs_entity_t e = regs[r].entity;
    if (!e) {
        return EcsWildcard;
    }
    
    ecs_check(ecs_is_valid(rule->world, e), ECS_INVALID_PARAMETER, NULL);   
    return e;
error:
    return 0;
}

static
void table_reg_set(
    const ecs_rule_t *rule,
    ecs_var_t *regs,
    int32_t r,
    ecs_table_t *table)
{
    (void)rule;
    ecs_assert(rule->vars[r].kind == EcsRuleVarKindTable, 
        ECS_INTERNAL_ERROR, NULL);

    regs[r].range.table = table;
    regs[r].range.offset = 0;
    regs[r].range.count = 0;
    regs[r].entity = 0;
}

static 
ecs_table_range_t table_reg_get(
    const ecs_rule_t *rule,
    ecs_var_t *regs,
    int32_t r)
{
    (void)rule;
    ecs_assert(rule->vars[r].kind == EcsRuleVarKindTable, 
        ECS_INTERNAL_ERROR, NULL);

    return regs[r].range;       
}

static
ecs_entity_t reg_get_entity(
    const ecs_rule_t *rule,
    ecs_rule_op_t *op,
    ecs_var_t *regs,
    int32_t r)
{
    if (r == UINT8_MAX) {
        ecs_assert(op->subject != 0, ECS_INTERNAL_ERROR, NULL);

        /* The subject is referenced from the query string by string identifier.
         * If subject entity is not valid, it could have been deletd by the
         * application after the rule was created */
        ecs_check(ecs_is_valid(rule->world, op->subject), 
            ECS_INVALID_PARAMETER, NULL);

        return op->subject;
    }
    if (rule->vars[r].kind == EcsRuleVarKindTable) {
        int32_t offset = regs[r].range.offset;

        ecs_assert(regs[r].range.count == 1, ECS_INTERNAL_ERROR, NULL);
        ecs_data_t *data = &table_reg_get(rule, regs, r).table->data;
        ecs_assert(data != NULL, ECS_INTERNAL_ERROR, NULL);
        ecs_entity_t *entities = ecs_storage_first(&data->entities);
        ecs_assert(entities != NULL, ECS_INTERNAL_ERROR, NULL);
        ecs_assert(offset < ecs_storage_count(&data->entities), 
            ECS_INTERNAL_ERROR, NULL);
        ecs_check(ecs_is_valid(rule->world, entities[offset]), 
            ECS_INVALID_PARAMETER, NULL);            
        
        return entities[offset];
    }
    if (rule->vars[r].kind == EcsRuleVarKindEntity) {
        return entity_reg_get(rule, regs, r);
    }

    /* Must return an entity */
    ecs_assert(false, ECS_INTERNAL_ERROR, NULL);

error:
    return 0;
}

static
ecs_table_range_t table_from_entity(
    const ecs_world_t *world,
    ecs_entity_t entity)
{
    ecs_assert(entity != 0, ECS_INTERNAL_ERROR, NULL);

    entity = ecs_get_alive(world, entity);
    
    ecs_table_range_t slice = {0};
    ecs_record_t *record = flecs_entities_get(world, entity);
    if (record) {
        slice.table = record->table;
        slice.offset = ECS_RECORD_TO_ROW(record->row);
        slice.count = 1;
    }

    return slice;
}

static
ecs_table_range_t reg_get_range(
    const ecs_rule_t *rule,
    ecs_rule_op_t *op,
    ecs_var_t *regs,
    int32_t r)
{
    if (r == UINT8_MAX) {
        ecs_check(ecs_is_valid(rule->world, op->subject), 
            ECS_INVALID_PARAMETER, NULL);
        return table_from_entity(rule->world, op->subject);
    }
    if (rule->vars[r].kind == EcsRuleVarKindTable) {
        return table_reg_get(rule, regs, r);
    }
    if (rule->vars[r].kind == EcsRuleVarKindEntity) {
        return table_from_entity(rule->world, entity_reg_get(rule, regs, r));
    } 
error:
    return (ecs_table_range_t){0};
}

static
void reg_set_entity(
    const ecs_rule_t *rule,
    ecs_var_t *regs,
    int32_t r,
    ecs_entity_t entity)
{
    if (rule->vars[r].kind == EcsRuleVarKindTable) {
        ecs_world_t *world = rule->world;
        ecs_check(ecs_is_valid(world, entity), ECS_INVALID_PARAMETER, NULL);
        regs[r].range = table_from_entity(world, entity);
        regs[r].entity = entity;
    } else {
        entity_reg_set(rule, regs, r, entity);
    }
error:
    return;
}

static
void reg_set_range(
    const ecs_rule_t *rule,
    ecs_var_t *regs,
    int32_t r,
    const ecs_table_range_t *range)
{
    if (rule->vars[r].kind == EcsRuleVarKindEntity) {
        ecs_check(range->count == 1, ECS_INTERNAL_ERROR, NULL);
        regs[r].range = *range;
        regs[r].entity = ecs_storage_get_t(&range->table->data.entities,
            ecs_entity_t, range->offset)[0];
    } else {
        regs[r].range = *range;
        regs[r].entity = 0;
    }
error:
    return;
}

/* This encodes a column expression into a pair. A pair stores information about
 * the variable(s) associated with the column. Pairs are used by operations to
 * apply filters, and when there is a match, to reify variables. */
static
ecs_rule_pair_t term_to_pair(
    ecs_rule_t *rule,
    ecs_term_t *term)
{
    ecs_rule_pair_t result = {0};

    /* Terms must always have at least one argument (the subject) */
    ecs_assert(subj_is_set(term), ECS_INTERNAL_ERROR, NULL);

    /* If the predicate id is a variable, find the variable and encode its id
     * in the pair so the operation can find it later. */
    if (term->first.flags & EcsIsVariable) {
        if (term->first.id != EcsAny) {
            /* Always lookup var as an entity, as pairs never refer to tables */
            const ecs_rule_var_t *var = find_variable(
                rule, EcsRuleVarKindEntity, term_id_var_name(&term->first));

            /* Variables should have been declared */
            ecs_assert(var != NULL, ECS_INTERNAL_ERROR, NULL);
            ecs_assert(var->kind == EcsRuleVarKindEntity, 
                ECS_INTERNAL_ERROR, NULL);
            result.first.reg = var->id;

            /* Set flag so the operation can see the predicate is a variable */
            result.reg_mask |= RULE_PAIR_PREDICATE;
            result.final = true;
        } else {
            result.first.id = EcsWildcard;
            result.final = true;
        }
    } else {
        /* If the predicate is not a variable, simply store its id. */
        ecs_entity_t pred_id = term->first.id;
        result.first.id = pred_id;

        /* Test if predicate is transitive. When evaluating the predicate, this
         * will also take into account transitive relationships */
        if (ecs_has_id(rule->world, pred_id, EcsTransitive)) {
            /* Transitive queries must have an object */
            if (obj_is_set(term)) {
                result.transitive = true;
            }
        }

        if (ecs_has_id(rule->world, pred_id, EcsFinal)) {
            result.final = true;
        }

        if (ecs_has_id(rule->world, pred_id, EcsReflexive)) {
            result.reflexive = true;
        }

        if (ecs_has_id(rule->world, pred_id, EcsAcyclic)) {
            result.acyclic = true;
        }
    }

    /* The pair doesn't do anything with the subject (sources are the things that
     * are matched against pairs) so if the column does not have a object, 
     * there is nothing left to do. */
    if (!obj_is_set(term)) {
        return result;
    }

    /* If arguments is higher than 2 this is not a pair but a nested rule */
    ecs_assert(obj_is_set(term), ECS_INTERNAL_ERROR, NULL);

    /* Same as above, if the object is a variable, store it and flag it */
    if (term->second.flags & EcsIsVariable) {
        if (term->second.id != EcsAny) {
            const ecs_rule_var_t *var = find_variable(
                rule, EcsRuleVarKindEntity, term_id_var_name(&term->second));

            /* Variables should have been declared */
            ecs_assert(var != NULL, ECS_INTERNAL_ERROR, 
                term_id_var_name(&term->second));
            ecs_assert(var->kind == EcsRuleVarKindEntity, ECS_INTERNAL_ERROR, 
                term_id_var_name(&term->second));

            result.second.reg = var->id;
            result.reg_mask |= RULE_PAIR_OBJECT;
        } else {
            result.second.id = EcsWildcard;
        }
    } else {
        /* If the object is not a variable, simply store its id */
        result.second.id = term->second.id;
        if (!result.second.id) {
            result.second_0 = true;
        }
    }

    return result;
}

/* When an operation has a pair, it is used to filter its input. This function
 * translates a pair back into an entity id, and in the process substitutes the
 * variables that have already been filled out. It's one of the most important
 * functions, as a lot of the filtering logic depends on having an entity that
 * has all of the reified variables correctly filled out. */
static
ecs_rule_filter_t pair_to_filter(
    ecs_rule_iter_t *it,
    ecs_rule_op_t *op,
    ecs_rule_pair_t pair)
{
    ecs_entity_t first = pair.first.id;
    ecs_entity_t second = pair.second.id;
    ecs_rule_filter_t result = {
        .lo_var = -1,
        .hi_var = -1
    };

    /* Get registers in case we need to resolve ids from registers. Get them
     * from the previous, not the current stack frame as the current operation
     * hasn't reified its variables yet. */
    ecs_var_t *regs = get_register_frame(it, op->frame - 1);

    if (pair.reg_mask & RULE_PAIR_OBJECT) {
        second = entity_reg_get(it->rule, regs, pair.second.reg);
        second = ecs_entity_t_lo(second); /* Filters don't have generations */

        if (second == EcsWildcard) {
            result.wildcard = true;
            result.second_wildcard = true;
            result.lo_var = pair.second.reg;
        }
    }

    if (pair.reg_mask & RULE_PAIR_PREDICATE) {
        first = entity_reg_get(it->rule, regs, pair.first.reg);
        first = ecs_entity_t_lo(first); /* Filters don't have generations */

        if (first == EcsWildcard) {
            if (result.wildcard) {
                result.same_var = pair.first.reg == pair.second.reg;
            }

            result.wildcard = true;
            result.first_wildcard = true;

            if (second) {
                result.hi_var = pair.first.reg;
            } else {
                result.lo_var = pair.first.reg;
            }
        }
    }

    if (!second && !pair.second_0) {
        result.mask = first;
    } else {
        result.mask = ecs_pair(first, second);
    }

    return result;
}

/* This function is responsible for reifying the variables (filling them out 
 * with their actual values as soon as they are known). It uses the pair 
 * expression returned by pair_get_most_specific_var, and attempts to fill out each of the
 * wildcards in the pair. If a variable isn't reified yet, the pair expression
 * will still contain one or more wildcards, which is harmless as the respective
 * registers will also point to a wildcard. */
static
void reify_variables(
    ecs_rule_iter_t *it,
    ecs_rule_op_t *op,
    ecs_rule_filter_t *filter,
    ecs_type_t type,
    int32_t column)
{
    const ecs_rule_t *rule = it->rule;
    const ecs_rule_var_t *vars = rule->vars;
    (void)vars;

    ecs_var_t *regs = get_registers(it, op);
    ecs_assert(column < type.count, ECS_INTERNAL_ERROR, NULL);
    ecs_entity_t *elem = &type.array[column];

    int32_t obj_var = filter->lo_var;
    int32_t pred_var = filter->hi_var;

    if (obj_var != -1) {
        ecs_assert(vars[obj_var].kind == EcsRuleVarKindEntity, 
            ECS_INTERNAL_ERROR, NULL);

        entity_reg_set(rule, regs, obj_var, 
            ecs_get_alive(rule->world, ECS_PAIR_SECOND(*elem)));
    }

    if (pred_var != -1) {
        ecs_assert(vars[pred_var].kind == EcsRuleVarKindEntity, 
            ECS_INTERNAL_ERROR, NULL);            

        entity_reg_set(rule, regs, pred_var, 
            ecs_get_alive(rule->world, 
                ECS_PAIR_FIRST(*elem)));
    }
}

/* Returns whether variable is a subject */
static
bool is_subject(
    ecs_rule_t *rule,
    ecs_rule_var_t *var)
{
    ecs_assert(rule != NULL, ECS_INTERNAL_ERROR, NULL);

    if (!var) {
        return false;
    }

    if (var->id < rule->subj_var_count) {
        return true;
    }

    return false;
}

static
bool skip_term(ecs_term_t *term) {
    if (ecs_term_match_0(term)) {
        return true;
    }
    return false;
}

static
int32_t get_variable_depth(
    ecs_rule_t *rule,
    ecs_rule_var_t *var,
    ecs_rule_var_t *root,
    int recur);

static
int32_t crawl_variable(
    ecs_rule_t *rule,
    ecs_rule_var_t *var,
    ecs_rule_var_t *root,
    int recur)    
{
    ecs_term_t *terms = rule->filter.terms;
    int32_t i, count = rule->filter.term_count;

    for (i = 0; i < count; i ++) {
        ecs_term_t *term = &terms[i];
        if (skip_term(term)) {
            continue;
        }

        ecs_rule_var_t 
        *first = term_pred(rule, term),
        *src = term_subj(rule, term),
        *second = term_obj(rule, term);

        /* Variable must at least appear once in term */
        if (var != first && var != src && var != second) {
            continue;
        }

        if (first && first != var && !first->marked) {
            get_variable_depth(rule, first, root, recur + 1);
        }

        if (src && src != var && !src->marked) {
            get_variable_depth(rule, src, root, recur + 1);
        }

        if (second && second != var && !second->marked) {
            get_variable_depth(rule, second, root, recur + 1);
        }
    }

    return 0;
}

static
int32_t get_depth_from_var(
    ecs_rule_t *rule,
    ecs_rule_var_t *var,
    ecs_rule_var_t *root,
    int recur)
{
    /* If variable is the root or if depth has been set, return depth + 1 */
    if (var == root || var->depth != UINT8_MAX) {
        return var->depth + 1;
    }

    /* Variable is already being evaluated, so this indicates a cycle. Stop */
    if (var->marked) {
        return 0;
    }
    
    /* Variable is not yet being evaluated and depth has not yet been set. 
     * Calculate depth. */
    int32_t depth = get_variable_depth(rule, var, root, recur + 1);
    if (depth == UINT8_MAX) {
        return depth;
    } else {
        return depth + 1;
    }
}

static
int32_t get_depth_from_term(
    ecs_rule_t *rule,
    ecs_rule_var_t *cur,
    ecs_rule_var_t *first,
    ecs_rule_var_t *second,
    ecs_rule_var_t *root,
    int recur)
{
    int32_t result = UINT8_MAX;

    /* If neither of the other parts of the terms are variables, this
     * variable is guaranteed to have no dependencies. */
    if (!first && !second) {
        result = 0;
    } else {
        /* If this is a variable that is not the same as the current, 
         * we can use it to determine dependency depth. */
        if (first && cur != first) {
            int32_t depth = get_depth_from_var(rule, first, root, recur);
            if (depth == UINT8_MAX) {
                return UINT8_MAX;
            }

            /* If the found depth is lower than the depth found, overwrite it */
            if (depth < result) {
                result = depth;
            }
        }

        /* Same for second */
        if (second && cur != second) {
            int32_t depth = get_depth_from_var(rule, second, root, recur);
            if (depth == UINT8_MAX) {
                return UINT8_MAX;
            }

            if (depth < result) {
                result = depth;
            }
        }
    }

    return result;
}

/* Find the depth of the dependency tree from the variable to the root */
static
int32_t get_variable_depth(
    ecs_rule_t *rule,
    ecs_rule_var_t *var,
    ecs_rule_var_t *root,
    int recur)
{
    var->marked = true;

    /* Iterate columns, find all instances where 'var' is not used as subject.
     * If the subject of that column is either the root or a variable for which
     * the depth is known, the depth for this variable can be determined. */
    ecs_term_t *terms = rule->filter.terms;

    int32_t i, count = rule->filter.term_count;
    int32_t result = UINT8_MAX;

    for (i = 0; i < count; i ++) {
        ecs_term_t *term = &terms[i];
        if (skip_term(term)) {
            continue;
        }

        ecs_rule_var_t 
        *first = term_pred(rule, term),
        *src = term_subj(rule, term),
        *second = term_obj(rule, term);

        if (src != var) {
            continue;
        }

        if (!is_subject(rule, first)) {
            first = NULL;
        }

        if (!is_subject(rule, second)) {
            second = NULL;
        }

        int32_t depth = get_depth_from_term(rule, var, first, second, root, recur);
        if (depth < result) {
            result = depth;
        }
    }

    if (result == UINT8_MAX) {
        result = 0;
    }

    var->depth = result;    

    /* Dependencies are calculated from subject to (first, second). If there were
     * sources that are only related by object (like (X, Y), (Z, Y)) it is
     * possible that those have not yet been found yet. To make sure those 
     * variables are found, loop again & follow predicate & object links */
    for (i = 0; i < count; i ++) {
        ecs_term_t *term = &terms[i];
        if (skip_term(term)) {
            continue;
        }

        ecs_rule_var_t 
        *src = term_subj(rule, term),
        *first = term_pred(rule, term),
        *second = term_obj(rule, term);

        /* Only evaluate first & second for current subject. This ensures that we
         * won't evaluate variables that are unreachable from the root. This
         * must be detected as unconstrained variables are not allowed. */
        if (src != var) {
            continue;
        }

        crawl_variable(rule, src, root, recur);

        if (first && first != var) {
            crawl_variable(rule, first, root, recur);
        }

        if (second && second != var) {
            crawl_variable(rule, second, root, recur);
        }        
    }

    return var->depth;
}

/* Compare function used for qsort. It ensures that variables are first ordered
 * by depth, followed by how often they occur. */
static
int compare_variable(
    const void* ptr1, 
    const void *ptr2)
{
    const ecs_rule_var_t *v1 = ptr1;
    const ecs_rule_var_t *v2 = ptr2;

    if (v1->kind < v2->kind) {
        return -1;
    } else if (v1->kind > v2->kind) {
        return 1;
    }

    if (v1->depth < v2->depth) {
        return -1;
    } else if (v1->depth > v2->depth) {
        return 1;
    }

    if (v1->occurs < v2->occurs) {
        return 1;
    } else {
        return -1;
    }

    return (v1->id < v2->id) - (v1->id > v2->id);
}

/* After all subject variables have been found, inserted and sorted, the 
 * remaining variables (predicate & object) still need to be inserted. This
 * function serves two purposes. The first purpose is to ensure that all 
 * variables are known before operations are emitted. This ensures that the
 * variables array won't be reallocated while emitting, which simplifies code.
 * The second purpose of the function is to ensure that if the root variable
 * (which, if it exists has now been created with a table type) is also inserted
 * with an entity type if required. This is used later to decide whether the
 * rule needs to insert an each instruction. */
static
int ensure_all_variables(
    ecs_rule_t *rule)
{
    ecs_term_t *terms = rule->filter.terms;
    int32_t i, count = rule->filter.term_count;

    for (i = 0; i < count; i ++) {
        ecs_term_t *term = &terms[i];
        if (skip_term(term)) {
            continue;
        }

        /* If predicate is a variable, make sure it has been registered */
        if (term->first.flags & EcsIsVariable) {
            if (ensure_term_id_variable(rule, term, &term->first) != 0) {
                return -1;
            }
        }

        /* If subject is a variable and it is not This, make sure it is 
         * registered as an entity variable. This ensures that the program will
         * correctly return all permutations */
        if (!ecs_term_match_this(term)) {
            if (ensure_term_id_variable(rule, term, &term->src) != 0) {
                return -1;
            }
        }

        /* If object is a variable, make sure it has been registered */
        if (obj_is_set(term) && (term->second.flags & EcsIsVariable)) {
            if (ensure_term_id_variable(rule, term, &term->second) != 0) {
                return -1;
            }
        }
    }

    return 0;   
}

/* Scan for variables, put them in optimal dependency order. */
static
int scan_variables(
    ecs_rule_t *rule)
{
    /* Objects found in rule. One will be elected root */
    int32_t subject_count = 0;

    /* If this (.) is found, it always takes precedence in root election */
    int32_t this_var = UINT8_MAX;

    /* Keep track of the subject variable that occurs the most. In the absence of
     * this (.) the variable with the most occurrences will be elected root. */
    int32_t max_occur = 0;
    int32_t max_occur_var = UINT8_MAX;

    /* Step 1: find all possible roots */
    ecs_term_t *terms = rule->filter.terms;
    int32_t i, term_count = rule->filter.term_count;

    for (i = 0; i < term_count; i ++) {
        ecs_term_t *term = &terms[i];

        /* Evaluate the subject. The predicate and object are not evaluated, 
         * since they never can be elected as root. */
        if (term_id_is_variable(&term->src)) {
            const char *subj_name = term_id_var_name(&term->src);
            
            ecs_rule_var_t *src = find_variable(
                rule, EcsRuleVarKindTable, subj_name);
            if (!src) {
                src = create_variable(rule, EcsRuleVarKindTable, subj_name);
                if (subject_count >= ECS_RULE_MAX_VAR_COUNT) {
                    rule_error(rule, "too many variables in rule");
                    goto error;
                }

                /* Make sure that variable name in term array matches with the
                 * rule name. */
                if (term->src.id != EcsThis && term->src.id != EcsAny) {
                    ecs_os_strset(&term->src.name, src->name);
                    term->src.id = 0;
                }
            }

            if (++ src->occurs > max_occur) {
                max_occur = src->occurs;
                max_occur_var = src->id;
            }
        }
    }

    rule->subj_var_count = rule->var_count;

    if (ensure_all_variables(rule) != 0) {
        goto error;
    }

    /* Variables in a term with a literal subject have depth 0 */
    for (i = 0; i < term_count; i ++) {
        ecs_term_t *term = &terms[i];

        if (!(term->src.flags & EcsIsVariable)) {
            ecs_rule_var_t 
            *first = term_pred(rule, term),
            *second = term_obj(rule, term);

            if (first) {
                first->depth = 0;
            }
            if (second) {
                second->depth = 0;
            }
        }
    }

    /* Elect a root. This is either this (.) or the variable with the most
     * occurrences. */
    int32_t root_var = this_var;
    if (root_var == UINT8_MAX) {
        root_var = max_occur_var;
        if (root_var == UINT8_MAX) {
            /* If no subject variables have been found, the rule expression only
             * operates on a fixed set of entities, in which case no root 
             * election is required. */
            goto done;
        }
    }

    ecs_rule_var_t *root = &rule->vars[root_var];
    root->depth = get_variable_depth(rule, root, root, 0);

    /* Verify that there are no unconstrained variables. Unconstrained variables
     * are variables that are unreachable from the root. */
    for (i = 0; i < rule->subj_var_count; i ++) {
        if (rule->vars[i].depth == UINT8_MAX) {
            rule_error(rule, "unconstrained variable '%s'", 
                rule->vars[i].name);
            goto error;
        } 
    }

    /* For each Not term, verify that variables are known */
    for (i = 0; i < term_count; i ++) {
        ecs_term_t *term = &terms[i];
        if (term->oper != EcsNot) {
            continue;
        }

        ecs_rule_var_t 
        *first = term_pred(rule, term),
        *second = term_obj(rule, term);

        if (!first && term_id_is_variable(&term->first) && 
            term->first.id != EcsAny)
        {
            rule_error(rule, "missing predicate variable '%s'", 
                term_id_var_name(&term->first));
            goto error;
        }
        if (!second && term_id_is_variable(&term->second) && 
            term->second.id != EcsAny)
        {
            rule_error(rule, "missing object variable '%s'", 
                term_id_var_name(&term->second));
            goto error;
        }
    }

    /* Order variables by depth, followed by occurrence. The variable
     * array will later be used to lead the iteration over the terms, and
     * determine which operations get inserted first. */
    int32_t var_count = rule->var_count;
    ecs_rule_var_t vars[ECS_RULE_MAX_VAR_COUNT];
    ecs_os_memcpy_n(vars, rule->vars, ecs_rule_var_t, var_count);
    ecs_qsort_t(&vars, var_count, ecs_rule_var_t, compare_variable);
    for (i = 0; i < var_count; i ++) {
        rule->var_eval_order[i] = vars[i].id;
    }

done:
    return 0;
error:
    return -1;
}

/* Get entity variable from table variable */
static
ecs_rule_var_t* to_entity(
    ecs_rule_t *rule,
    ecs_rule_var_t *var)
{
    if (!var) {
        return NULL;
    }

    ecs_rule_var_t *evar = NULL;
    if (var->kind == EcsRuleVarKindTable) {
        evar = find_variable(rule, EcsRuleVarKindEntity, var->name);
    } else {
        evar = var;
    }

    return evar;
}

/* Ensure that if a table variable has been written, the corresponding entity
 * variable is populated. The function will return the most specific, populated
 * variable. */
static
ecs_rule_var_t* most_specific_var(
    ecs_rule_t *rule,
    ecs_rule_var_t *var,
    bool *written,
    bool create)
{
    if (!var) {
        return NULL;
    }

    ecs_rule_var_t *tvar, *evar = to_entity(rule, var);
    if (!evar) {
        return var;
    }

    if (var->kind == EcsRuleVarKindTable) {
        tvar = var;
    } else {
        tvar = find_variable(rule, EcsRuleVarKindTable, var->name);
    }

    /* If variable is used as predicate or object, it should have been 
     * registered as an entity. */
    ecs_assert(evar != NULL, ECS_INTERNAL_ERROR, NULL);

    /* Usually table variables are resolved before they are used as a predicate
     * or object, but in the case of cyclic dependencies this is not guaranteed.
     * Only insert an each instruction of the table variable has been written */
    if (tvar && written[tvar->id]) {
        /* If the variable has been written as a table but not yet
         * as an entity, insert an each operation that yields each
         * entity in the table. */
        if (evar) {
            if (written[evar->id]) {
                return evar;
            } else if (create) {
                ecs_rule_op_t *op = create_operation(rule);
                op->kind = EcsRuleEach;
                op->on_pass = rule->operation_count;
                op->on_fail = rule->operation_count - 2;
                op->frame = rule->frame_count;
                op->has_in = true;
                op->has_out = true;
                op->r_in = tvar->id;
                op->r_out = evar->id;

                /* Entity will either be written or has been written */
                written[evar->id] = true;

                push_frame(rule);

                return evar;
            } else {
                return tvar;
            }
        }
    } else if (evar && written[evar->id]) {
        return evar;
    }

    return var;
}

/* Get most specific known variable */
static
ecs_rule_var_t *get_most_specific_var(
    ecs_rule_t *rule,
    ecs_rule_var_t *var,
    bool *written)
{
    return most_specific_var(rule, var, written, false);
}

/* Get or create most specific known variable. This will populate an entity
 * variable if a table variable is known but the entity variable isn't. */
static
ecs_rule_var_t *ensure_most_specific_var(
    ecs_rule_t *rule,
    ecs_rule_var_t *var,
    bool *written)
{
    return most_specific_var(rule, var, written, true);
}


/* Ensure that an entity variable is written before using it */
static
ecs_rule_var_t* ensure_entity_written(
    ecs_rule_t *rule,
    ecs_rule_var_t *var,
    bool *written)
{
    if (!var) {
        return NULL;
    }

    /* Ensure we're working with the most specific version of src we can get */
    ecs_rule_var_t *evar = ensure_most_specific_var(rule, var, written);

    /* The post condition of this function is that there is an entity variable,
     * and that it is written. Make sure that the result is an entity */
    ecs_assert(evar != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(evar->kind == EcsRuleVarKindEntity, ECS_INTERNAL_ERROR, NULL);

    /* Make sure the variable has been written */
    ecs_assert(written[evar->id] == true, ECS_INTERNAL_ERROR, NULL);
    
    return evar;
}

static
ecs_rule_op_t* insert_operation(
    ecs_rule_t *rule,
    int32_t term_index,
    bool *written)
{
    ecs_rule_pair_t pair = {0};

    /* Parse the term's type into a pair. A pair extracts the ids from
     * the term, and replaces variables with wildcards which can then
     * be matched against actual relationships. A pair retains the 
     * information about the variables, so that when a match happens,
     * the pair can be used to reify the variable. */
    if (term_index != -1) {
        ecs_term_t *term = &rule->filter.terms[term_index];

        pair = term_to_pair(rule, term);

        /* If the pair contains entity variables that have not yet been written,
         * insert each instructions in case their tables are known. Variables in
         * a pair that are truly unknown will be populated by the operation,
         * but an operation should never overwrite an entity variable if the 
         * corresponding table variable has already been resolved. */
        if (pair.reg_mask & RULE_PAIR_PREDICATE) {
            ecs_rule_var_t *first = &rule->vars[pair.first.reg];
            first = get_most_specific_var(rule, first, written);
            pair.first.reg = first->id;
        }

        if (pair.reg_mask & RULE_PAIR_OBJECT) {
            ecs_rule_var_t *second = &rule->vars[pair.second.reg];
            second = get_most_specific_var(rule, second, written);
            pair.second.reg = second->id;
        }
    } else {
        /* Not all operations have a filter (like Each) */
    }

    ecs_rule_op_t *op = create_operation(rule);
    op->on_pass = rule->operation_count;
    op->on_fail = rule->operation_count - 2;
    op->frame = rule->frame_count;
    op->filter = pair;

    /* Store corresponding signature term so we can correlate and
     * store the table columns with signature columns. */
    op->term = term_index;

    return op;
}

/* Insert first operation, which is always Input. This creates an entry in
 * the register stack for the initial state. */
static
void insert_input(
    ecs_rule_t *rule)
{
    ecs_rule_op_t *op = create_operation(rule);
    op->kind = EcsRuleInput;

    /* The first time Input is evaluated it goes to the next/first operation */
    op->on_pass = 1;

    /* When Input is evaluated with redo = true it will return false, which will
     * finish the program as op becomes -1. */
    op->on_fail = -1;  

    push_frame(rule);
}

/* Insert last operation, which is always Yield. When the program hits Yield,
 * data is returned to the application. */
static
void insert_yield(
    ecs_rule_t *rule)
{
    ecs_rule_op_t *op = create_operation(rule);
    op->kind = EcsRuleYield;
    op->has_in = true;
    op->on_fail = rule->operation_count - 2;
    /* Yield can only "fail" since it is the end of the program */

    /* Find variable associated with this. It is possible that the variable
     * exists both as a table and as an entity. This can happen when a rule
     * first selects a table for this, but then subsequently needs to evaluate
     * each entity in that table. In that case the yield instruction should
     * return the entity, so look for that first. */
    ecs_rule_var_t *var = find_variable(rule, EcsRuleVarKindEntity, ".");
    if (!var) {
        var = find_variable(rule, EcsRuleVarKindTable, ".");
    }

    /* If there is no this, there is nothing to yield. In that case the rule
     * simply returns true or false. */
    if (!var) {
        op->r_in = UINT8_MAX;
    } else {
        op->r_in = var->id;
    }

    op->frame = push_frame(rule);
}

/* Return superset/subset including the root */
static
void insert_reflexive_set(
    ecs_rule_t *rule,
    ecs_rule_op_kind_t op_kind,
    ecs_rule_var_t *out,
    const ecs_rule_pair_t pair,
    int32_t c,
    bool *written,
    bool reflexive)
{
    ecs_assert(out != NULL, ECS_INTERNAL_ERROR, NULL);

    ecs_rule_var_t *first = pair_pred(rule, &pair);
    ecs_rule_var_t *second = pair_obj(rule, &pair);

    int32_t setjmp_lbl = rule->operation_count;
    int32_t store_lbl = setjmp_lbl + 1;
    int32_t set_lbl = setjmp_lbl + 2;
    int32_t next_op = setjmp_lbl + 4;
    int32_t prev_op = setjmp_lbl - 1;

    /* Insert 4 operations at once, so we don't have to worry about how
     * the instruction array reallocs. If operation is not reflexive, we only
     * need to insert the set operation. */
    if (reflexive) {
        insert_operation(rule, -1, written);
        insert_operation(rule, -1, written);
        insert_operation(rule, -1, written);
    }

    ecs_rule_op_t *op = insert_operation(rule, -1, written);
    ecs_rule_op_t *setjmp = &rule->operations[setjmp_lbl];
    ecs_rule_op_t *store = &rule->operations[store_lbl];
    ecs_rule_op_t *set = &rule->operations[set_lbl];
    ecs_rule_op_t *jump = op;

    if (!reflexive) {
        set_lbl = setjmp_lbl;
        set = op;
        setjmp = NULL;
        store = NULL;
        jump = NULL;
        next_op = set_lbl + 1;
        prev_op = set_lbl - 1;
    }

    /* The SetJmp operation stores a conditional jump label that either
     * points to the Store or *Set operation */
    if (reflexive) {
        setjmp->kind = EcsRuleSetJmp;
        setjmp->on_pass = store_lbl;
        setjmp->on_fail = set_lbl;
    }

    /* The Store operation yields the root of the subtree. After yielding,
     * this operation will fail and return to SetJmp, which will cause it
     * to switch to the *Set operation. */
    if (reflexive) {
        store->kind = EcsRuleStore;
        store->on_pass = next_op;
        store->on_fail = setjmp_lbl;
        store->has_in = true;
        store->has_out = true;
        store->r_out = out->id;
        store->term = c;

        if (!first) {
            store->filter.first = pair.first;
        } else {
            store->filter.first.reg = first->id;
            store->filter.reg_mask |= RULE_PAIR_PREDICATE;
        }

        /* If the object of the filter is not a variable, store literal */
        if (!second) {
            store->r_in = UINT8_MAX;
            store->subject = ecs_get_alive(rule->world, pair.second.id);
            store->filter.second = pair.second;
        } else {
            store->r_in = second->id;
            store->filter.second.reg = second->id;
            store->filter.reg_mask |= RULE_PAIR_OBJECT;
        }
    }

    /* This is either a SubSet or SuperSet operation */
    set->kind = op_kind;
    set->on_pass = next_op;
    set->on_fail = prev_op;
    set->has_out = true;
    set->r_out = out->id;
    set->term = c;

    /* Predicate can be a variable if it's non-final */
    if (!first) {
        set->filter.first = pair.first;
    } else {
        set->filter.first.reg = first->id;
        set->filter.reg_mask |= RULE_PAIR_PREDICATE;
    }

    if (!second) {
        set->filter.second = pair.second;
    } else {
        set->filter.second.reg = second->id;
        set->filter.reg_mask |= RULE_PAIR_OBJECT;
    }

    if (reflexive) {
        /* The jump operation jumps to either the store or subset operation,
        * depending on whether the store operation already yielded. The 
        * operation is inserted last, so that the on_fail label of the next 
        * operation will point to it */
        jump->kind = EcsRuleJump;
        
        /* The pass/fail labels of the Jump operation are not used, since it
        * jumps to a variable location. Instead, the pass label is (ab)used to
        * store the label of the SetJmp operation, so that the jump can access
        * the label it needs to jump to from the setjmp op_ctx. */
        jump->on_pass = setjmp_lbl;
        jump->on_fail = -1;
    }

    written[out->id] = true;
}

static
ecs_rule_var_t* store_reflexive_set(
    ecs_rule_t *rule,
    ecs_rule_op_kind_t op_kind,
    ecs_rule_pair_t *pair,
    bool *written,
    bool reflexive,
    bool as_entity)
{
    /* Ensure we're using the most specific version of second */
    ecs_rule_var_t *second = pair_obj(rule, pair);
    if (second) {
        pair->second.reg = second->id;
    }

    ecs_rule_var_kind_t var_kind = EcsRuleVarKindTable;

    /* Create anonymous variable for storing the set */
    ecs_rule_var_t *av = create_anonymous_variable(rule, var_kind);
    int32_t ave_id = 0, av_id = av->id;

    /* If the variable kind is a table, also create an entity variable as the
     * result of the set operation should be returned as an entity */
    if (var_kind == EcsRuleVarKindTable && as_entity) {
        create_variable(rule, EcsRuleVarKindEntity, av->name);
        av = &rule->vars[av_id];
        ave_id = av_id + 1;
    }

    /* Generate the operations */
    insert_reflexive_set(rule, op_kind, av, *pair, -1, written, reflexive);

    /* Make sure to return entity variable, and that it is populated */
    if (as_entity) {
        return ensure_entity_written(rule, &rule->vars[ave_id], written);
    } else {
        return &rule->vars[av_id];
    }
}

static
bool is_known(
    ecs_rule_var_t *var,
    bool *written)
{
    if (!var) {
        return true;
    } else {
        return written[var->id];
    }
}

static
bool is_pair_known(
    ecs_rule_t *rule,
    ecs_rule_pair_t *pair,
    bool *written)
{
    ecs_rule_var_t *pred_var = pair_pred(rule, pair);
    if (!is_known(pred_var, written) || pair->first.id == EcsWildcard) {
        return false;
    }

    ecs_rule_var_t *obj_var = pair_obj(rule, pair);
    if (!is_known(obj_var, written) || pair->second.id == EcsWildcard) {
        return false;
    }

    return true;
}

static
void set_input_to_subj(
    ecs_rule_t *rule,
    ecs_rule_op_t *op,
    ecs_term_t *term,
    ecs_rule_var_t *var)
{
    (void)rule;
    
    op->has_in = true;
    if (!var) {
        op->r_in = UINT8_MAX;
        op->subject = term->src.id;

        /* Invalid entities should have been caught during parsing */
        ecs_assert(ecs_is_valid(rule->world, op->subject), 
            ECS_INTERNAL_ERROR, NULL);
    } else {
        op->r_in = var->id;
    }
}

static
void set_output_to_subj(
    ecs_rule_t *rule,
    ecs_rule_op_t *op,
    ecs_term_t *term,
    ecs_rule_var_t *var)
{
    (void)rule;

    op->has_out = true;
    if (!var) {
        op->r_out = UINT8_MAX;
        op->subject = term->src.id;

        /* Invalid entities should have been caught during parsing */
        ecs_assert(ecs_is_valid(rule->world, op->subject), 
            ECS_INTERNAL_ERROR, NULL);
    } else {
        op->r_out = var->id;
    }
}

static
void insert_select_or_with(
    ecs_rule_t *rule,
    int32_t c,
    ecs_term_t *term,
    ecs_rule_var_t *src,
    ecs_rule_pair_t *pair,
    bool *written)
{
    ecs_rule_op_t *op;
    bool eval_subject_supersets = false;

    /* Find any entity and/or table variables for subject */
    ecs_rule_var_t *tvar = NULL, *evar = to_entity(rule, src), *var = evar;
    if (src && src->kind == EcsRuleVarKindTable) {
        tvar = src;
        if (!evar) {
            var = tvar;
        }
    }

    int32_t lbl_start = rule->operation_count;
    ecs_rule_pair_t filter;
    if (pair) {
        filter = *pair;
    } else {
        filter = term_to_pair(rule, term);
    }

    /* Only insert implicit IsA if filter isn't already an IsA */
    if ((!filter.transitive || filter.first.id != EcsIsA) && term->oper != EcsNot) {
        if (!var) {
            ecs_rule_pair_t isa_pair = {
                .first.id = EcsIsA,
                .second.id = term->src.id
            };

            evar = src = store_reflexive_set(rule, EcsRuleSuperSet, &isa_pair, 
                written, true, true);
            tvar = NULL;
            eval_subject_supersets = true;

        } else if (ecs_id_is_wildcard(term->id) && 
            ECS_PAIR_FIRST(term->id) != EcsThis &&
            ECS_PAIR_SECOND(term->id) != EcsThis)
        {
            ecs_assert(src != NULL, ECS_INTERNAL_ERROR, NULL);

            op = insert_operation(rule, -1, written);

            if (!is_known(src, written)) {
                op->kind = EcsRuleSelect;
                set_output_to_subj(rule, op, term, src);
                written[src->id] = true;
            } else {
                op->kind = EcsRuleWith;
                set_input_to_subj(rule, op, term, src);
            }

            ecs_rule_pair_t isa_pair = {
                .first.id = EcsIsA,
                .second.reg = src->id,
                .reg_mask = RULE_PAIR_OBJECT
            };

            op->filter = filter;
            if (op->filter.reg_mask & RULE_PAIR_PREDICATE) {
                op->filter.first.id = EcsWildcard;
            }
            if (op->filter.reg_mask & RULE_PAIR_OBJECT) {
                op->filter.second.id = EcsWildcard;
            }
            op->filter.reg_mask = 0;

            push_frame(rule);

            tvar = src = store_reflexive_set(rule, EcsRuleSuperSet, &isa_pair, 
                written, true, false);

            evar = NULL;
        }
    }

    /* If no pair is provided, create operation from specified term */
    if (!pair) {
        op = insert_operation(rule, c, written);

    /* If an explicit pair is provided, override the default one from the 
     * term. This allows for using a predicate or object variable different
     * from what is in the term. One application of this is to substitute a
     * predicate with its subsets, if it is non final */
    } else {
        op = insert_operation(rule, -1, written);
        op->filter = *pair;

        /* Assign the term id, so that the operation will still be correctly
         * associated with the correct expression term. */
        op->term = c;
    }

    /* If entity variable is known and resolved, create with for it */
    if (evar && is_known(evar, written)) {
        op->kind = EcsRuleWith;
        op->r_in = evar->id;
        set_input_to_subj(rule, op, term, src);

    /* If table variable is known and resolved, create with for it */
    } else if (tvar && is_known(tvar, written)) {
        op->kind = EcsRuleWith;
        op->r_in = tvar->id;
        set_input_to_subj(rule, op, term, src);

    /* If subject is neither table nor entitiy, with operates on literal */        
    } else if (!tvar && !evar) {
        op->kind = EcsRuleWith;
        set_input_to_subj(rule, op, term, src);

    /* If subject is table or entity but not known, use select */
    } else {
        ecs_assert(src != NULL, ECS_INTERNAL_ERROR, NULL);
        op->kind = EcsRuleSelect;
        set_output_to_subj(rule, op, term, src);
        written[src->id] = true;
    }

    /* If supersets of subject are being evaluated, and we're looking for a
     * specific filter, stop as soon as the filter has been matched. */
    if (eval_subject_supersets && is_pair_known(rule, &op->filter, written)) {
        op = insert_operation(rule, -1, written);

        /* When the next operation returns, it will first hit SetJmp with a redo
         * which will switch the jump label to the previous operation */
        op->kind = EcsRuleSetJmp;
        op->on_pass = rule->operation_count;
        op->on_fail = lbl_start - 1;
    }

    if (op->filter.reg_mask & RULE_PAIR_PREDICATE) {
        written[op->filter.first.reg] = true;
    }

    if (op->filter.reg_mask & RULE_PAIR_OBJECT) {
        written[op->filter.second.reg] = true;
    }
}

static
void prepare_predicate(
    ecs_rule_t *rule,
    ecs_rule_pair_t *pair,
    int32_t term,
    bool *written)  
{
    /* If pair is not final, resolve term for all IsA relationships of the
     * predicate. Note that if the pair has final set to true, it is guaranteed
     * that the predicate can be used in an IsA query */
    if (!pair->final) {
        ecs_rule_pair_t isa_pair = {
            .first.id = EcsIsA,
            .second.id = pair->first.id
        };

        ecs_rule_var_t *first = store_reflexive_set(rule, EcsRuleSubSet, 
            &isa_pair, written, true, true);

        pair->first.reg = first->id;
        pair->reg_mask |= RULE_PAIR_PREDICATE;

        if (term != -1) {
            rule->term_vars[term].first = first->id;
        }
    }
}

static
void insert_term_2(
    ecs_rule_t *rule,
    ecs_term_t *term,
    ecs_rule_pair_t *filter,
    int32_t c,
    bool *written)
{
    int32_t subj_id = -1, obj_id = -1;
    ecs_rule_var_t *src = term_subj(rule, term);
    if ((src = get_most_specific_var(rule, src, written))) {
        subj_id = src->id;
    }

    ecs_rule_var_t *second = term_obj(rule, term);
    if ((second = get_most_specific_var(rule, second, written))) {
        obj_id = second->id;
    }

    bool subj_known = is_known(src, written);
    bool same_obj_subj = false;
    if (src && second) {
        same_obj_subj = !ecs_os_strcmp(src->name, second->name);
    }

    if (!filter->transitive) {
        insert_select_or_with(rule, c, term, src, filter, written);
        if (src) src = &rule->vars[subj_id];
        if (second) second = &rule->vars[obj_id];

    } else if (filter->transitive) {
        if (subj_known) {
            if (is_known(second, written)) {
                if (filter->second.id != EcsWildcard) {
                    ecs_rule_var_t *obj_subsets = store_reflexive_set(
                        rule, EcsRuleSubSet, filter, written, true, true);

                    if (src) {
                        src = &rule->vars[subj_id];
                    }

                    rule->term_vars[c].second = obj_subsets->id;

                    ecs_rule_pair_t pair = *filter;
                    pair.second.reg = obj_subsets->id;
                    pair.reg_mask |= RULE_PAIR_OBJECT;

                    insert_select_or_with(rule, c, term, src, &pair, written);
                } else {
                    insert_select_or_with(rule, c, term, src, filter, written);
                }
            } else {
                ecs_assert(second != NULL, ECS_INTERNAL_ERROR, NULL);

                /* If subject is literal, find supersets for subject */
                if (src == NULL || src->kind == EcsRuleVarKindEntity) {
                    second = to_entity(rule, second);

                    ecs_rule_pair_t set_pair = *filter;
                    set_pair.reg_mask &= RULE_PAIR_PREDICATE;

                    if (src) {
                        set_pair.second.reg = src->id;
                        set_pair.reg_mask |= RULE_PAIR_OBJECT;
                    } else {
                        set_pair.second.id = term->src.id;
                    }

                    insert_reflexive_set(rule, EcsRuleSuperSet, second, set_pair, 
                        c, written, filter->reflexive);

                /* If subject is variable, first find matching pair for the 
                 * evaluated entity(s) and return supersets */
                } else {
                    ecs_rule_var_t *av = create_anonymous_variable(
                        rule, EcsRuleVarKindEntity);

                    src = &rule->vars[subj_id];
                    second = &rule->vars[obj_id];
                    second = to_entity(rule, second);

                    ecs_rule_pair_t set_pair = *filter;
                    set_pair.second.reg = av->id;
                    set_pair.reg_mask |= RULE_PAIR_OBJECT;

                    /* Insert with to find initial object for relationship */
                    insert_select_or_with(
                        rule, c, term, src, &set_pair, written);

                    push_frame(rule);

                    /* Find supersets for returned initial object. Make sure
                     * this is always reflexive since it needs to return the
                     * object from the pair that the entity has itself. */
                    insert_reflexive_set(rule, EcsRuleSuperSet, second, set_pair, 
                        c, written, true);
                }
            }

        /* src is not known */
        } else {
            ecs_assert(src != NULL, ECS_INTERNAL_ERROR, NULL);

            if (is_known(second, written)) {
                ecs_rule_pair_t set_pair = *filter;
                set_pair.reg_mask &= RULE_PAIR_PREDICATE; /* clear object mask */

                if (second) {
                    set_pair.second.reg = second->id;
                    set_pair.reg_mask |= RULE_PAIR_OBJECT;
                } else {
                    set_pair.second.id = term->second.id;
                }

                if (second) {
                    rule->term_vars[c].second = second->id;
                } else {
                    ecs_rule_var_t *av = create_anonymous_variable(rule,
                        EcsRuleVarKindEntity);
                    rule->term_vars[c].second = av->id;
                    written[av->id] = true;
                }

                insert_reflexive_set(rule, EcsRuleSubSet, src, set_pair, c, 
                    written, filter->reflexive);
            } else if (src == second) {
                insert_select_or_with(rule, c, term, src, filter, written);
            } else {
                ecs_assert(second != NULL, ECS_INTERNAL_ERROR, NULL);

                ecs_rule_var_t *av = NULL;
                if (!filter->reflexive) {
                    av = create_anonymous_variable(rule, EcsRuleVarKindEntity);
                }

                src = &rule->vars[subj_id];
                second = &rule->vars[obj_id];
                second = to_entity(rule, second);

                /* Insert instruction to find all sources and objects */
                ecs_rule_op_t *op = insert_operation(rule, -1, written);
                op->kind = EcsRuleSelect;
                set_output_to_subj(rule, op, term, src);
                op->filter.first = filter->first;

                if (filter->reflexive) {
                    op->filter.second.id = EcsWildcard;
                    op->filter.reg_mask = filter->reg_mask & RULE_PAIR_PREDICATE;
                } else {
                    op->filter.second.reg = av->id;
                    op->filter.reg_mask = filter->reg_mask | RULE_PAIR_OBJECT;
                    written[av->id] = true;
                }

                written[src->id] = true;

                /* Create new frame for operations that create reflexive set */
                push_frame(rule);

                /* Insert superset instruction to find all supersets */
                if (filter->reflexive) {
                    src = ensure_most_specific_var(rule, src, written);
                    ecs_assert(src->kind == EcsRuleVarKindEntity, 
                        ECS_INTERNAL_ERROR, NULL);
                    ecs_assert(written[src->id] == true,
                        ECS_INTERNAL_ERROR, NULL);

                    ecs_rule_pair_t super_filter = {0};
                    super_filter.first = filter->first;
                    super_filter.second.reg = src->id;
                    super_filter.reg_mask = filter->reg_mask | RULE_PAIR_OBJECT;

                    insert_reflexive_set(rule, EcsRuleSuperSet, second, 
                        super_filter, c, written, true);
                } else {
                    insert_reflexive_set(rule, EcsRuleSuperSet, second, 
                        op->filter, c, written, true);
                }
            }
        }
    }

    if (same_obj_subj) {
        /* Can't have relationship with same variables that is acyclic and not
         * reflexive, this should've been caught earlier. */
        ecs_assert(!filter->acyclic || filter->reflexive, 
            ECS_INTERNAL_ERROR, NULL);

        /* If relationship is reflexive and entity has an instance of R, no checks
         * are needed because R(X, X) is always true. */
        if (!filter->reflexive) {
            push_frame(rule);

            /* Insert check if the (R, X) pair that was found matches with one
             * of the entities in the table with the pair. */
            ecs_rule_op_t *op = insert_operation(rule, -1, written);
            second = get_most_specific_var(rule, second, written);
            ecs_assert(second->kind == EcsRuleVarKindEntity, 
                ECS_INTERNAL_ERROR, NULL);
            ecs_assert(written[src->id] == true, ECS_INTERNAL_ERROR, NULL);
            ecs_assert(written[second->id] == true, ECS_INTERNAL_ERROR, NULL);
            
            set_input_to_subj(rule, op, term, src);
            op->filter.second.reg = second->id;
            op->filter.reg_mask = RULE_PAIR_OBJECT;

            if (src->kind == EcsRuleVarKindTable) {
                op->kind = EcsRuleInTable;
            } else {
                op->kind = EcsRuleEq;
            }
        }
    }
}

static
void insert_term_1(
    ecs_rule_t *rule,
    ecs_term_t *term,
    ecs_rule_pair_t *filter,
    int32_t c,
    bool *written)
{
    ecs_rule_var_t *src = term_subj(rule, term);
    src = get_most_specific_var(rule, src, written);
    insert_select_or_with(rule, c, term, src, filter, written);
}

static
void insert_term(
    ecs_rule_t *rule,
    ecs_term_t *term,
    int32_t c,
    bool *written)
{
    bool obj_set = obj_is_set(term);

    ensure_most_specific_var(rule, term_pred(rule, term), written);
    if (obj_set) {
        ensure_most_specific_var(rule, term_obj(rule, term), written);
    }

    /* If term has Not operator, prepend Not which turns a fail into a pass */
    int32_t prev = rule->operation_count;
    ecs_rule_op_t *not_pre;
    if (term->oper == EcsNot) {
        not_pre = insert_operation(rule, -1, written);
        not_pre->kind = EcsRuleNot;
        not_pre->has_in = false;
        not_pre->has_out = false;
    }

    ecs_rule_pair_t filter = term_to_pair(rule, term);
    prepare_predicate(rule, &filter, c, written);

    if (subj_is_set(term) && !obj_set) {
        insert_term_1(rule, term, &filter, c, written);
    } else if (obj_set) {
        insert_term_2(rule, term, &filter, c, written);
    }

    /* If term has Not operator, append Not which turns a pass into a fail */
    if (term->oper == EcsNot) {
        ecs_rule_op_t *not_post = insert_operation(rule, -1, written);
        not_post->kind = EcsRuleNot;
        not_post->has_in = false;
        not_post->has_out = false;

        not_post->on_pass = prev - 1;
        not_post->on_fail = prev - 1;
        not_pre = &rule->operations[prev];
        not_pre->on_fail = rule->operation_count;
    }

    if (term->oper == EcsOptional) {
        /* Insert Not instruction that ensures that the optional term is only
         * executed once */
        ecs_rule_op_t *jump = insert_operation(rule, -1, written);
        jump->kind = EcsRuleNot;
        jump->has_in = false;
        jump->has_out = false;
        jump->on_pass = rule->operation_count;
        jump->on_fail = prev - 1;

        /* Find exit instruction for optional term, and make the fail label
         * point to the Not operation, so that even when the operation fails,
         * it won't discard the result */
        int i, min_fail = -1, exit_op = -1;
        for (i = prev; i < rule->operation_count; i ++) {
            ecs_rule_op_t *op = &rule->operations[i];
            if (min_fail == -1 || (op->on_fail >= 0 && op->on_fail < min_fail)){
                min_fail = op->on_fail;
                exit_op = i;
            }
        }

        ecs_assert(exit_op != -1, ECS_INTERNAL_ERROR, NULL);
        ecs_rule_op_t *op = &rule->operations[exit_op];
        op->on_fail = rule->operation_count - 1;
    }

    push_frame(rule);
}

/* Create program from operations that will execute the query */
static
void compile_program(
    ecs_rule_t *rule)
{
    /* Trace which variables have been written while inserting instructions.
     * This determines which instruction needs to be inserted */
    bool written[ECS_RULE_MAX_VAR_COUNT] = { false };

    ecs_term_t *terms = rule->filter.terms;
    int32_t v, c, term_count = rule->filter.term_count;
    ecs_rule_op_t *op;

    /* Insert input, which is always the first instruction */
    insert_input(rule);

    /* First insert all instructions that do not have a variable subject. Such
     * instructions iterate the type of an entity literal and are usually good
     * candidates for quickly narrowing down the set of potential results. */
    for (c = 0; c < term_count; c ++) {
        ecs_term_t *term = &terms[c];
        if (skip_term(term)) {
            continue;
        }

        if (term->oper == EcsOptional || term->oper == EcsNot) {
            continue;
        }

        ecs_rule_var_t* src = term_subj(rule, term);
        if (src) {
            continue;
        }

        insert_term(rule, term, c, written);
    }

    /* Insert variables based on dependency order */
    for (v = 0; v < rule->subj_var_count; v ++) {
        int32_t var_id = rule->var_eval_order[v];
        ecs_rule_var_t *var = &rule->vars[var_id];

        ecs_assert(var->kind == EcsRuleVarKindTable, ECS_INTERNAL_ERROR, NULL);

        for (c = 0; c < term_count; c ++) {
            ecs_term_t *term = &terms[c];
            if (skip_term(term)) {
                continue;
            }

            if (term->oper == EcsOptional || term->oper == EcsNot) {
                continue;
            }

            /* Only process columns for which variable is subject */
            ecs_rule_var_t* src = term_subj(rule, term);
            if (src != var) {
                continue;
            }

            insert_term(rule, term, c, written);

            var = &rule->vars[var_id];
        }
    }

    /* Insert terms with Not operators */
    for (c = 0; c < term_count; c ++) {
        ecs_term_t *term = &terms[c];
        if (term->oper != EcsNot) {
            continue;
        }

        insert_term(rule, term, c, written);
    }

    /* Insert terms with Optional operators last, as optional terms cannot 
     * eliminate results, and would just add overhead to evaluation of 
     * non-matching entities. */
    for (c = 0; c < term_count; c ++) {
        ecs_term_t *term = &terms[c];
        if (term->oper != EcsOptional) {
            continue;
        }
        
        insert_term(rule, term, c, written);
    }

    /* Verify all subject variables have been written. Source variables are of
     * the table type, and a select/subset should have been inserted for each */
    for (v = 0; v < rule->subj_var_count; v ++) {
        if (!written[v]) {
            /* If the table variable hasn't been written, this can only happen
             * if an instruction wrote the variable before a select/subset could
             * have been inserted for it. Make sure that this is the case by
             * testing if an entity variable exists and whether it has been
             * written. */
            ecs_rule_var_t *var = find_variable(
                rule, EcsRuleVarKindEntity, rule->vars[v].name);
            ecs_assert(var != NULL, ECS_INTERNAL_ERROR, NULL);
            ecs_assert(written[var->id], ECS_INTERNAL_ERROR, var->name);
            (void)var;
        }
    }

    /* Make sure that all entity variables are written. With the exception of
     * the this variable, which can be returned as a table, other variables need
     * to be available as entities. This ensures that all permutations for all
     * variables are correctly returned by the iterator. When an entity variable
     * hasn't been written yet at this point, it is because it only constrained
     * through a common predicate or object. */
    for (; v < rule->var_count; v ++) {
        if (!written[v]) {
            ecs_rule_var_t *var = &rule->vars[v];
            ecs_assert(var->kind == EcsRuleVarKindEntity, 
                ECS_INTERNAL_ERROR, NULL);

            ecs_rule_var_t *table_var = find_variable(
                rule, EcsRuleVarKindTable, var->name);
            
            /* A table variable must exist if the variable hasn't been resolved
             * yet. If there doesn't exist one, this could indicate an 
             * unconstrained variable which should have been caught earlier */
            ecs_assert(table_var != NULL, ECS_INTERNAL_ERROR, var->name);

            /* Insert each operation that takes the table variable as input, and
             * yields each entity in the table */
            op = insert_operation(rule, -1, written);
            op->kind = EcsRuleEach;
            op->r_in = table_var->id;
            op->r_out = var->id;
            op->frame = rule->frame_count;
            op->has_in = true;
            op->has_out = true;
            written[var->id] = true;
            
            push_frame(rule);
        }
    }     

    /* Insert yield, which is always the last operation */
    insert_yield(rule);
}

static
void create_variable_name_array(
    ecs_rule_t *rule)
{
    if (rule->var_count) {
        int i;
        for (i = 0; i < rule->var_count; i ++) {
            ecs_rule_var_t *var = &rule->vars[i];

            if (var->kind != EcsRuleVarKindEntity) {
                /* Table variables are hidden for applications. */
                rule->var_names[var->id] = NULL;
            } else {
                rule->var_names[var->id] = var->name;
            }
        }
    }
}

static
void create_variable_cross_references(
    ecs_rule_t *rule)
{
    if (rule->var_count) {
        int i;
        for (i = 0; i < rule->var_count; i ++) {
            ecs_rule_var_t *var = &rule->vars[i];
            if (var->kind == EcsRuleVarKindEntity) {
                ecs_rule_var_t *tvar = find_variable(
                    rule, EcsRuleVarKindTable, var->name);
                if (tvar) {
                    var->other = tvar->id;
                } else {
                    var->other = -1;
                }
            } else {
                ecs_rule_var_t *evar = find_variable(
                    rule, EcsRuleVarKindEntity, var->name);
                if (evar) {
                    var->other = evar->id;
                } else {
                    var->other = -1;
                }
            }
        }
    }
}

/* Implementation for iterable mixin */
static
void rule_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_rule_t);

    if (filter) {
        iter[1] = ecs_rule_iter(world, (ecs_rule_t*)poly);
        iter[0] = ecs_term_chain_iter(&iter[1], filter);
    } else {
        iter[0] = ecs_rule_iter(world, (ecs_rule_t*)poly);
    }
}

static
int32_t find_term_var_id(
    ecs_rule_t *rule,
    ecs_term_id_t *term_id)
{
    if (term_id_is_variable(term_id)) {
        const char *var_name = term_id_var_name(term_id);
        ecs_rule_var_t *var = find_variable(
            rule, EcsRuleVarKindEntity, var_name);
        if (var) {
            return var->id;
        } else {
            /* If this is Any look for table variable. Since Any is only
             * required to return a single result, there is no need to 
             * insert an each instruction for a matching table. */
            if (term_id->id == EcsAny) {
                var = find_variable(
                    rule, EcsRuleVarKindTable, var_name);
                if (var) {
                    return var->id;
                }
            }
        }
    }
    
    return -1;
}

ecs_rule_t* ecs_rule_init(
    ecs_world_t *world,
    const ecs_filter_desc_t *const_desc)
{
    ecs_rule_t *result = ecs_poly_new(ecs_rule_t);

    /* Initialize the query */
    ecs_filter_desc_t desc = *const_desc;
    desc.storage = &result->filter; /* Use storage of rule */
    result->filter = ECS_FILTER_INIT;
    if (ecs_filter_init(world, &desc) == NULL) {
        goto error;
    }

    result->world = world;

    /* Rule has no terms */
    if (!result->filter.term_count) {
        rule_error(result, "rule has no terms");
        goto error;
    }

    ecs_term_t *terms = result->filter.terms;
    int32_t i, term_count = result->filter.term_count;

    /* Make sure rule doesn't just have Not terms */
    for (i = 0; i < term_count; i++) {
        ecs_term_t *term = &terms[i];
        if (term->oper != EcsNot) {
            break;
        }
    }
    if (i == term_count) {
        rule_error(result, "rule cannot only have terms with Not operator");
        goto error;
    }

    /* Translate terms with a Not operator and Wildcard to Any, as we don't need
     * implicit variables for those */
    for (i = 0; i < term_count; i ++) {
        ecs_term_t *term = &terms[i];
        if (term->oper == EcsNot) {
            if (term->first.id == EcsWildcard) {
                term->first.id = EcsAny;
            }
            if (term->src.id == EcsWildcard) {
                term->src.id = EcsAny;
            }
            if (term->second.id == EcsWildcard) {
                term->second.id = EcsAny;
            }
        }
    }

    /* Find all variables & resolve dependencies */
    if (scan_variables(result) != 0) {
        goto error;
    }

    /* Create lookup array for subject variables */
    for (i = 0; i < term_count; i ++) {
        ecs_term_t *term = &terms[i];
        ecs_rule_term_vars_t *vars = &result->term_vars[i];
        vars->first = find_term_var_id(result, &term->first);
        vars->src = find_term_var_id(result, &term->src);
        vars->second = find_term_var_id(result, &term->second);
    }

    /* Generate the opcode array */
    compile_program(result);

    /* Create array with variable names so this can be easily accessed by 
     * iterators without requiring access to the ecs_rule_t */
    create_variable_name_array(result);

    /* Create cross-references between variables so it's easy to go from entity
     * to table variable and vice versa */
    create_variable_cross_references(result);

    result->iterable.init = rule_iter_init;

    return result;
error:
    ecs_rule_fini(result);
    return NULL;
}

void ecs_rule_fini(
    ecs_rule_t *rule)
{
    int32_t i;
    for (i = 0; i < rule->var_count; i ++) {
        ecs_os_free(rule->vars[i].name);
    }

    ecs_filter_fini(&rule->filter);

    ecs_os_free(rule->operations);
    ecs_os_free(rule);
}

const ecs_filter_t* ecs_rule_get_filter(
    const ecs_rule_t *rule)
{
    return &rule->filter; 
}

/* Quick convenience function to get a variable from an id */
static
ecs_rule_var_t* get_variable(
    const ecs_rule_t *rule,
    int32_t var_id)
{
    if (var_id == UINT8_MAX) {
        return NULL;
    }

    return (ecs_rule_var_t*)&rule->vars[var_id];
}

/* Convert the program to a string. This can be useful to analyze how a rule is
 * being evaluated. */
char* ecs_rule_str(
    ecs_rule_t *rule)
{
    ecs_check(rule != NULL, ECS_INVALID_PARAMETER, NULL);
    
    ecs_world_t *world = rule->world;
    ecs_strbuf_t buf = ECS_STRBUF_INIT;
    char filter_expr[256];

    int32_t i, count = rule->operation_count;
    for (i = 1; i < count; i ++) {
        ecs_rule_op_t *op = &rule->operations[i];
        ecs_rule_pair_t pair = op->filter;
        ecs_entity_t first = pair.first.id;
        ecs_entity_t second = pair.second.id;
        const char *pred_name = NULL, *obj_name = NULL;
        char *pred_name_alloc = NULL, *obj_name_alloc = NULL;

        if (pair.reg_mask & RULE_PAIR_PREDICATE) {
            ecs_assert(rule->vars != NULL, ECS_INTERNAL_ERROR, NULL);
            ecs_rule_var_t *type_var = &rule->vars[pair.first.reg];
            pred_name = type_var->name;
        } else if (first) {
            pred_name_alloc = ecs_get_fullpath(world, ecs_get_alive(world, first));
            pred_name = pred_name_alloc;
        }

        if (pair.reg_mask & RULE_PAIR_OBJECT) {
            ecs_assert(rule->vars != NULL, ECS_INTERNAL_ERROR, NULL);
            ecs_rule_var_t *obj_var = &rule->vars[pair.second.reg];
            obj_name = obj_var->name;
        } else if (second) {
            obj_name_alloc = ecs_get_fullpath(world, ecs_get_alive(world, second));
            obj_name = obj_name_alloc;
        } else if (pair.second_0) {
            obj_name = "0";
        }

        ecs_strbuf_append(&buf, "%2d: [S:%2d, P:%2d, F:%2d, T:%2d] ", i, 
            op->frame, op->on_pass, op->on_fail, op->term);

        bool has_filter = false;

        switch(op->kind) {
        case EcsRuleSelect:
            ecs_strbuf_append(&buf, "select   ");
            has_filter = true;
            break;
        case EcsRuleWith:
            ecs_strbuf_append(&buf, "with     ");
            has_filter = true;
            break;
        case EcsRuleStore:
            ecs_strbuf_append(&buf, "store    ");
            break;
        case EcsRuleSuperSet:
            ecs_strbuf_append(&buf, "superset ");
            has_filter = true;
            break;             
        case EcsRuleSubSet:
            ecs_strbuf_append(&buf, "subset   ");
            has_filter = true;
            break;            
        case EcsRuleEach:
            ecs_strbuf_append(&buf, "each     ");
            break;
        case EcsRuleSetJmp:
            ecs_strbuf_append(&buf, "setjmp   ");
            break;
        case EcsRuleJump:
            ecs_strbuf_append(&buf, "jump     ");
            break;
        case EcsRuleNot:
            ecs_strbuf_append(&buf, "not      ");
            break;
        case EcsRuleInTable:
            ecs_strbuf_append(&buf, "intable  ");
            has_filter = true;
            break;
        case EcsRuleEq:
            ecs_strbuf_append(&buf, "eq       ");
            has_filter = true;
            break;            
        case EcsRuleYield:
            ecs_strbuf_append(&buf, "yield    ");
            break;
        default:
            continue;
        }

        if (op->has_out) {
            ecs_rule_var_t *r_out = get_variable(rule, op->r_out);
            if (r_out) {
                ecs_strbuf_append(&buf, "O:%s%s ", 
                    r_out->kind == EcsRuleVarKindTable ? "t" : "",
                    r_out->name);
            } else if (op->subject) {
                char *subj_path = ecs_get_fullpath(world, op->subject);
                ecs_strbuf_append(&buf, "O:%s ", subj_path);
                ecs_os_free(subj_path);
            }
        }

        if (op->has_in) {
            ecs_rule_var_t *r_in = get_variable(rule, op->r_in);
            if (r_in) {
                ecs_strbuf_append(&buf, "I:%s%s ", 
                    r_in->kind == EcsRuleVarKindTable ? "t" : "",
                    r_in->name);
            }
            if (op->subject) {
                char *subj_path = ecs_get_fullpath(world, op->subject);
                ecs_strbuf_append(&buf, "I:%s ", subj_path);
                ecs_os_free(subj_path);
            }
        }

        if (has_filter) {
            if (!pred_name) {
                pred_name = "-";
            }
            if (!obj_name && !pair.second_0) {
                ecs_os_sprintf(filter_expr, "(%s)", pred_name);
            } else {
                ecs_os_sprintf(filter_expr, "(%s, %s)", pred_name, obj_name);
            }
            ecs_strbuf_append(&buf, "F:%s", filter_expr);
        }

        ecs_strbuf_appendstr(&buf, "\n");

        ecs_os_free(pred_name_alloc);
        ecs_os_free(obj_name_alloc);
    }

    return ecs_strbuf_get(&buf);
error:
    return NULL;
}

/* Public function that returns number of variables. This enables an application
 * to iterate the variables and obtain their values. */
int32_t ecs_rule_var_count(
    const ecs_rule_t *rule)
{
    ecs_assert(rule != NULL, ECS_INTERNAL_ERROR, NULL);
    return rule->var_count;
}

/* Public function to find a variable by name */
int32_t ecs_rule_find_var(
    const ecs_rule_t *rule,
    const char *name)
{
    ecs_rule_var_t *v = find_variable(rule, EcsRuleVarKindEntity, name);
    if (v) {
        return v->id;
    } else {
        return -1;
    }
}

/* Public function to get the name of a variable. */
const char* ecs_rule_var_name(
    const ecs_rule_t *rule,
    int32_t var_id)
{
    return rule->vars[var_id].name;
}

/* Public function to get the type of a variable. */
bool ecs_rule_var_is_entity(
    const ecs_rule_t *rule,
    int32_t var_id)
{
    return rule->vars[var_id].kind == EcsRuleVarKindEntity;
}

static
void ecs_rule_iter_free(
    ecs_iter_t *iter)
{
    ecs_rule_iter_t *it = &iter->priv.iter.rule;
    ecs_os_free(it->registers);
    ecs_os_free(it->columns);
    ecs_os_free(it->op_ctx);
    iter->columns = NULL;
    it->registers = NULL;
    it->columns = NULL;
    it->op_ctx = NULL;
}

/* Create rule iterator */
ecs_iter_t ecs_rule_iter(
    const ecs_world_t *world,
    const ecs_rule_t *rule)
{
    ecs_assert(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(rule != NULL, ECS_INVALID_PARAMETER, NULL);

    ecs_iter_t result = {0};
    int i;

    result.world = (ecs_world_t*)world;
    result.real_world = (ecs_world_t*)ecs_get_world(rule->world);

    flecs_process_pending_tables(result.real_world);

    ecs_rule_iter_t *it = &result.priv.iter.rule;
    it->rule = rule;

    if (rule->operation_count) {
        if (rule->var_count) {
            it->registers = ecs_os_malloc_n(ecs_var_t, 
                rule->operation_count * rule->var_count);
        }
        
        it->op_ctx = ecs_os_calloc_n(ecs_rule_op_ctx_t, rule->operation_count);

        if (rule->filter.term_count) {
            it->columns = ecs_os_malloc_n(int32_t, 
                rule->operation_count * rule->filter.term_count);
        }

        for (i = 0; i < rule->filter.term_count; i ++) {
            it->columns[i] = -1;
        }
    }

    it->op = 0;

    for (i = 0; i < rule->var_count; i ++) {
        if (rule->vars[i].kind == EcsRuleVarKindEntity) {
            entity_reg_set(rule, it->registers, i, EcsWildcard);
        } else {
            table_reg_set(rule, it->registers, i, NULL);
        }
    }

    result.variable_names = (char**)rule->var_names;
    result.variable_count = rule->var_count;
    result.term_count = rule->filter.term_count;
    result.terms = rule->filter.terms;
    result.next = ecs_rule_next;
    result.fini = ecs_rule_iter_free;
    ECS_BIT_COND(result.flags, EcsIterIsFilter, 
        ECS_BIT_IS_SET(rule->filter.flags, EcsFilterIsFilter));

    flecs_iter_init(&result, 
        flecs_iter_cache_ids |
        /* flecs_iter_cache_columns | provided by rule iterator */
        flecs_iter_cache_sources |
        flecs_iter_cache_sizes |
        flecs_iter_cache_ptrs |
        /* flecs_iter_cache_match_indices | not necessary for iteration */
        flecs_iter_cache_variables);

    result.columns = it->columns; /* prevent alloc */

    return result;
}

/* Edge case: if the filter has the same variable for both predicate and
 * object, they are both resolved at the same time but at the time of 
 * evaluating the filter they're still wildcards which would match columns
 * that have different predicates/objects. Do an additional scan to make
 * sure the column we're returning actually matches. */
static
int32_t find_next_same_var(
    ecs_type_t type,
    int32_t column,
    ecs_id_t pattern)
{
    /* If same_var is true, this has to be a wildcard pair. We cannot have
     * the same variable in a pair, and one part of a pair resolved with
     * another part unresolved. */
    ecs_assert(pattern == ecs_pair(EcsWildcard, EcsWildcard), 
        ECS_INTERNAL_ERROR, NULL);
    (void)pattern;
    
    /* Keep scanning for an id where rel and second are the same */
    ecs_id_t *ids = type.array;
    int32_t i, count = type.count;
    for (i = column + 1; i < count; i ++) {
        ecs_id_t id = ids[i];
        if (!ECS_HAS_ROLE(id, PAIR)) {
            /* If id is not a pair, this will definitely not match, and we
             * will find no further matches. */
            return -1;
        }

        if (ECS_PAIR_FIRST(id) == ECS_PAIR_SECOND(id)) {
            /* Found a match! */
            return i;
        }
    }

    /* No pairs found with same rel/second */
    return -1;
}

static
int32_t find_next_column(
    const ecs_world_t *world,
    const ecs_table_t *table,
    int32_t column,
    ecs_rule_filter_t *filter)
{
    ecs_assert(table != NULL, ECS_INTERNAL_ERROR, NULL);

    ecs_entity_t pattern = filter->mask;
    ecs_type_t type = table->type;

    if (column == -1) {
        ecs_table_record_t *tr = flecs_table_record_get(world, table, pattern);
        if (!tr) {
            return -1;
        }
        column = tr->column;
    } else {   
        column = ecs_search_offset(world, table, column + 1, filter->mask, 0);
        if (column == -1) {
            return -1;
        }
    }

    if (filter->same_var) {
        column = find_next_same_var(type, column - 1, filter->mask);
    }

    return column;
}

/* This function finds the next table in a table set, and is used by the select
 * operation. The function automatically skips empty tables, so that subsequent
 * operations don't waste a lot of processing for nothing. */
static
ecs_table_record_t find_next_table(
    ecs_rule_filter_t *filter,
    ecs_rule_with_ctx_t *op_ctx)
{
    ecs_table_cache_iter_t *it = &op_ctx->it;
    ecs_table_t *table = NULL;
    int32_t column = -1;

    const ecs_table_record_t *tr;
    while ((column == -1) && (tr = flecs_table_cache_next(it, ecs_table_record_t))) {
        table = tr->hdr.table;

        /* Should only iterate non-empty tables */
        ecs_assert(ecs_table_count(table) != 0, ECS_INTERNAL_ERROR, NULL);

        column = tr->column;
        if (filter->same_var) {
            column = find_next_same_var(table->type, column - 1, filter->mask);
        }
    }

    if (column == -1) {
        table = NULL;
    }

    return (ecs_table_record_t){.hdr.table = table, .column = column};
}


static
ecs_id_record_t* find_tables(
    ecs_world_t *world,
    ecs_id_t id)
{
    ecs_id_record_t *idr = flecs_id_record_get(world, id);
    if (!idr || !flecs_table_cache_count(&idr->cache)) {
        /* Skip ids that don't have (non-empty) tables */
        return NULL;
    }
    return idr;
}

static
ecs_id_t rule_get_column(
    ecs_type_t type,
    int32_t column)
{
    ecs_assert(column < type.count, ECS_INTERNAL_ERROR, NULL);
    ecs_id_t *comp = &type.array[column];
    return *comp;
}

static
void set_source(
    ecs_iter_t *it,
    ecs_rule_op_t *op,
    ecs_var_t *regs,
    int32_t r)
{
    if (op->term == -1) {
        /* If operation is not associated with a term, don't set anything */
        return;
    }

    ecs_assert(op->term >= 0, ECS_INTERNAL_ERROR, NULL);

    const ecs_rule_t *rule = it->priv.iter.rule.rule;
    if ((r != UINT8_MAX) && rule->vars[r].kind == EcsRuleVarKindEntity) {
        it->sources[op->term] = reg_get_entity(rule, op, regs, r);
    } else {
        it->sources[op->term] = 0;
    }
}

static
void set_term_vars(
    const ecs_rule_t *rule,
    ecs_var_t *regs,
    int32_t term,
    ecs_id_t id)
{
    if (term != -1) {
        ecs_world_t *world = rule->world;
        const ecs_rule_term_vars_t *vars = &rule->term_vars[term];
        if (vars->first != -1) {
            regs[vars->first].entity = ecs_pair_first(world, id);
            ecs_assert(ecs_is_valid(world, regs[vars->first].entity), 
                ECS_INTERNAL_ERROR, NULL);
        }
        if (vars->second != -1) {
            regs[vars->second].entity = ecs_pair_second(world, id);
            ecs_assert(ecs_is_valid(world, regs[vars->second].entity), 
                ECS_INTERNAL_ERROR, NULL);
        }
    }
}

/* Input operation. The input operation acts as a placeholder for the start of
 * the program, and creates an entry in the register array that can serve to
 * store variables passed to an iterator. */
static
bool eval_input(
    ecs_iter_t *it,
    ecs_rule_op_t *op,
    int32_t op_index,
    bool redo)
{
    (void)it;
    (void)op;
    (void)op_index;

    if (!redo) {
        /* First operation executed by the iterator. Always return true. */
        return true;
    } else {
        /* When Input is asked to redo, it means that all other operations have
         * exhausted their results. Input itself does not yield anything, so
         * return false. This will terminate rule execution. */
        return false;
    }
}

static
bool eval_superset(
    ecs_iter_t *it,
    ecs_rule_op_t *op,
    int32_t op_index,
    bool redo)
{
    ecs_rule_iter_t *iter = &it->priv.iter.rule;
    const ecs_rule_t  *rule = iter->rule;
    ecs_world_t *world = rule->world;
    ecs_rule_superset_ctx_t *op_ctx = &iter->op_ctx[op_index].is.superset;
    ecs_rule_superset_frame_t *frame = NULL;
    ecs_var_t *regs = get_registers(iter, op);

    /* Get register indices for output */
    int32_t sp;
    int32_t r = op->r_out;

    /* Register cannot be a literal, since we need to store things in it */
    ecs_assert(r != UINT8_MAX, ECS_INTERNAL_ERROR, NULL);

    /* Get queried for id, fill out potential variables */
    ecs_rule_pair_t pair = op->filter;

    ecs_rule_filter_t filter = pair_to_filter(iter, op, pair);
    ecs_entity_t rel = ECS_PAIR_FIRST(filter.mask);
    ecs_rule_filter_t super_filter = { 
        .mask = ecs_pair(rel, EcsWildcard) 
    };
    ecs_table_t *table = NULL;

    /* Check if input register is constrained */
    ecs_entity_t result = iter->registers[r].entity;
    bool output_is_input = ecs_iter_var_is_constrained(it, r);
    if (output_is_input && !redo) {
        ecs_assert(regs[r].entity == iter->registers[r].entity, 
            ECS_INTERNAL_ERROR, NULL);
    }

    if (!redo) {
        op_ctx->stack = op_ctx->storage;
        sp = op_ctx->sp = 0;
        frame = &op_ctx->stack[sp];

        /* Get table of object for which to get supersets */
        ecs_entity_t second = ECS_PAIR_SECOND(filter.mask);
        if (second == EcsWildcard) {
            ecs_assert(pair.reg_mask & RULE_PAIR_OBJECT, 
                ECS_INTERNAL_ERROR, NULL);
            table = regs[pair.second.reg].range.table;
        } else {
            table = table_from_entity(world, second).table;
        }

        int32_t column;

        /* If output variable is already set, check if it matches */
        if (output_is_input) {
            ecs_id_t id = ecs_pair(rel, result);
            ecs_entity_t src = 0;
            column = ecs_search_relation(world, table, 0, id, rel, 
                0, &src, 0, 0);
            if (column != -1) {
                if (src != 0) {
                    table = ecs_get_table(world, src);
                }
            }
        } else {
            column = find_next_column(world, table, -1, &super_filter);
        }

        /* If no matching column was found, there are no supersets */
        if (column == -1) {
            return false;
        }

        ecs_assert(table != NULL, ECS_INTERNAL_ERROR, NULL);

        ecs_id_t col_id = rule_get_column(table->type, column);
        ecs_assert(ECS_HAS_ROLE(col_id, PAIR), ECS_INTERNAL_ERROR, NULL);
        ecs_entity_t col_obj = ecs_pair_second(world, col_id);

        reg_set_entity(rule, regs, r, col_obj);

        frame->table = table;
        frame->column = column;

        return true;
    } else if (output_is_input) {
        return false;
    }

    sp = op_ctx->sp;
    frame = &op_ctx->stack[sp];
    table = frame->table;
    int32_t column = frame->column;

    ecs_id_t col_id = rule_get_column(table->type, column);
    ecs_entity_t col_obj = ecs_pair_second(world, col_id);
    ecs_table_t *next_table = table_from_entity(world, col_obj).table;

    if (next_table) {
        sp ++;
        frame = &op_ctx->stack[sp];
        frame->table = next_table;
        frame->column = -1;
    }

    do {
        frame = &op_ctx->stack[sp];
        table = frame->table;
        column = frame->column;

        column = find_next_column(world, table, column, &super_filter);
        if (column != -1) {
            op_ctx->sp = sp;
            frame->column = column;
            col_id = rule_get_column(table->type, column);
            col_obj = ecs_pair_second(world, col_id);
            reg_set_entity(rule, regs, r, col_obj);
            return true;        
        }

        sp --;
    } while (sp >= 0);

    return false;
}

static
bool eval_subset(
    ecs_iter_t *it,
    ecs_rule_op_t *op,
    int32_t op_index,
    bool redo)
{
    ecs_rule_iter_t *iter = &it->priv.iter.rule;
    const ecs_rule_t  *rule = iter->rule;
    ecs_world_t *world = rule->world;
    ecs_rule_subset_ctx_t *op_ctx = &iter->op_ctx[op_index].is.subset;
    ecs_rule_subset_frame_t *frame = NULL;
    ecs_table_record_t table_record;
    ecs_var_t *regs = get_registers(iter, op);

    /* Get register indices for output */
    int32_t sp, row;
    int32_t r = op->r_out;
    ecs_assert(r != UINT8_MAX, ECS_INTERNAL_ERROR, NULL);

    /* Get queried for id, fill out potential variables */
    ecs_rule_pair_t pair = op->filter;
    ecs_rule_filter_t filter = pair_to_filter(iter, op, pair);
    ecs_id_record_t *idr;
    ecs_table_t *table = NULL;

    if (!redo) {
        op_ctx->stack = op_ctx->storage;
        sp = op_ctx->sp = 0;
        frame = &op_ctx->stack[sp];
        idr = frame->with_ctx.idr = find_tables(world, filter.mask);
        if (!idr) {
            return false;
        }

        flecs_table_cache_iter(&idr->cache, &frame->with_ctx.it);
        table_record = find_next_table(&filter, &frame->with_ctx);
        
        /* If first table set has no non-empty table, yield nothing */
        if (!table_record.hdr.table) {
            return false;
        }

        frame->row = 0;
        frame->column = table_record.column;
        table_reg_set(rule, regs, r, (frame->table = table_record.hdr.table));
        goto yield;
    }

    do {
        sp = op_ctx->sp;
        frame = &op_ctx->stack[sp];
        table = frame->table;
        row = frame->row;

        /* If row exceeds number of elements in table, find next table in frame that
         * still has entities */
        while ((sp >= 0) && (row >= ecs_table_count(table))) {
            table_record = find_next_table(&filter, &frame->with_ctx);

            if (table_record.hdr.table) {
                table = frame->table = table_record.hdr.table;
                ecs_assert(table != NULL, ECS_INTERNAL_ERROR, NULL);
                frame->row = 0;
                frame->column = table_record.column;
                table_reg_set(rule, regs, r, table);
                goto yield;
            } else {
                sp = -- op_ctx->sp;
                if (sp < 0) {
                    /* If none of the frames yielded anything, no more data */
                    return false;
                }
                frame = &op_ctx->stack[sp];
                table = frame->table;
                idr = frame->with_ctx.idr;
                row = ++ frame->row;

                ecs_assert(table != NULL, ECS_INTERNAL_ERROR, NULL);
                ecs_assert(idr != NULL, ECS_INTERNAL_ERROR, NULL);
            }
        }

        int32_t row_count = ecs_table_count(table);

        /* Table must have at least row elements */
        ecs_assert(row_count > row, ECS_INTERNAL_ERROR, NULL);

        ecs_entity_t *entities = ecs_storage_first(&table->data.entities);
        ecs_assert(entities != NULL, ECS_INTERNAL_ERROR, NULL);

        /* The entity used to find the next table set */
        do {
            ecs_entity_t e = entities[row];

            /* Create look_for expression with the resolved entity as object */
            pair.reg_mask &= ~RULE_PAIR_OBJECT; /* turn of bit because it's not a reg */
            pair.second.id = e;
            filter = pair_to_filter(iter, op, pair);

            /* Find table set for expression */
            table = NULL;
            idr = find_tables(world, filter.mask);

            /* If table set is found, find first non-empty table */
            if (idr) {
                ecs_rule_subset_frame_t *new_frame = &op_ctx->stack[sp + 1];
                new_frame->with_ctx.idr = idr;
                flecs_table_cache_iter(&idr->cache, &new_frame->with_ctx.it);
                table_record = find_next_table(&filter, &new_frame->with_ctx);

                /* If set contains non-empty table, push it to stack */
                if (table_record.hdr.table) {
                    table = table_record.hdr.table;
                    op_ctx->sp ++;
                    new_frame->table = table;
                    new_frame->row = 0;
                    new_frame->column = table_record.column;
                    frame = new_frame;
                }
            }

            /* If no table was found for the current entity, advance row */
            if (!table) {
                row = ++ frame->row;
            }
        } while (!table && row < row_count);
    } while (!table);

    table_reg_set(rule, regs, r, table);

yield:
    set_term_vars(rule, regs, op->term, frame->table->type.array[frame->column]);

    return true;
}

/* Select operation. The select operation finds and iterates a table set that
 * corresponds to its pair expression.  */
static
bool eval_select(
    ecs_iter_t *it,
    ecs_rule_op_t *op,
    int32_t op_index,
    bool redo)
{
    ecs_rule_iter_t *iter = &it->priv.iter.rule;
    const ecs_rule_t  *rule = iter->rule;
    ecs_world_t *world = rule->world;
    ecs_rule_with_ctx_t *op_ctx = &iter->op_ctx[op_index].is.with;
    ecs_table_record_t table_record;
    ecs_var_t *regs = get_registers(iter, op);

    /* Get register indices for output */
    int32_t r = op->r_out;
    ecs_assert(r != UINT8_MAX, ECS_INTERNAL_ERROR, NULL);

    /* Get queried for id, fill out potential variables */
    ecs_rule_pair_t pair = op->filter;
    ecs_rule_filter_t filter = pair_to_filter(iter, op, pair);
    ecs_entity_t pattern = filter.mask;
    int32_t *columns = rule_get_columns(iter, op);

    int32_t column = -1;
    ecs_table_t *table = NULL;
    ecs_id_record_t *idr;

    if (!redo && op->term != -1) {
        columns[op->term] = -1;
    }

    /* If this is a redo, we already looked up the table set */
    if (redo) {
        idr = op_ctx->idr;
    
    /* If this is not a redo lookup the table set. Even though this may not be
     * the first time the operation is evaluated, variables may have changed
     * since last time, which could change the table set to lookup. */
    } else {
        /* A table set is a set of tables that all contain at least the 
         * requested look_for expression. What is returned is a table record, 
         * which in addition to the table also stores the first occurrance at
         * which the requested expression occurs in the table. This reduces (and
         * in most cases eliminates) any searching that needs to occur in a
         * table type. Tables are also registered under wildcards, which is why
         * this operation can simply use the look_for variable directly */

        idr = op_ctx->idr = find_tables(world, pattern);
    }

    /* If no table set was found for queried for entity, there are no results */
    if (!idr) {
        return false;
    }

    /* If the input register is not NULL, this is a variable that's been set by
     * the application. */
    table = iter->registers[r].range.table;
    bool output_is_input = table != NULL;

    if (output_is_input && !redo) {
        ecs_assert(regs[r].range.table == iter->registers[r].range.table, 
            ECS_INTERNAL_ERROR, NULL);

        table = iter->registers[r].range.table;

        /* Check if table can be found in the id record. If not, the provided 
        * table does not match with the query. */
        ecs_table_record_t *tr = ecs_table_cache_get(&idr->cache, table);
        if (!tr) {
            return false;
        }

        column = op_ctx->column = tr->column;
    }

    /* If this is not a redo, start at the beginning */
    if (!redo) {
        if (!table) {
            flecs_table_cache_iter(&idr->cache, &op_ctx->it);

            /* Return the first table_record in the table set. */
            table_record = find_next_table(&filter, op_ctx);
        
            /* If no table record was found, there are no results. */
            if (!table_record.hdr.table) {
                return false;
            }

            table = table_record.hdr.table;

            /* Set current column to first occurrence of queried for entity */
            column = op_ctx->column = table_record.column;

            /* Store table in register */
            table_reg_set(rule, regs, r, table);
        }
    
    /* If this is a redo, progress to the next match */
    } else {
        /* First test if there are any more matches for the current table, in 
         * case we're looking for a wildcard. */
        if (filter.wildcard) {
            table = table_reg_get(rule, regs, r).table;
            ecs_assert(table != NULL, ECS_INTERNAL_ERROR, NULL);

            column = op_ctx->column;
            column = find_next_column(world, table, column, &filter);
            op_ctx->column = column;
        }

        /* If no next match was found for this table, move to next table */
        if (column == -1) {
            if (output_is_input) {
                return false;
            }

            table_record = find_next_table(&filter, op_ctx);
            if (!table_record.hdr.table) {
                return false;
            }

            /* Assign new table to table register */
            table_reg_set(rule, regs, r, (table = table_record.hdr.table));

            /* Assign first matching column */
            column = op_ctx->column = table_record.column;
        }
    }

    /* If we got here, we found a match. Table and column must be set */
    ecs_assert(table != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(column != -1, ECS_INTERNAL_ERROR, NULL);

    if (op->term != -1) {
        columns[op->term] = column;
    }

    /* If this is a wildcard query, fill out the variable registers */
    if (filter.wildcard) {
        reify_variables(iter, op, &filter, table->type, column);
    }

    return true;
}

/* With operation. The With operation always comes after either the Select or
 * another With operation, and applies additional filters to the table. */
static
bool eval_with(
    ecs_iter_t *it,
    ecs_rule_op_t *op,
    int32_t op_index,
    bool redo)
{
    ecs_rule_iter_t *iter = &it->priv.iter.rule;
    const ecs_rule_t *rule = iter->rule;
    ecs_world_t *world = rule->world;
    ecs_rule_with_ctx_t *op_ctx = &iter->op_ctx[op_index].is.with;
    ecs_var_t *regs = get_registers(iter, op);

    /* Get register indices for input */
    int32_t r = op->r_in;

    /* Get queried for id, fill out potential variables */
    ecs_rule_pair_t pair = op->filter;
    ecs_rule_filter_t filter = pair_to_filter(iter, op, pair);
    int32_t *columns = rule_get_columns(iter, op);

    /* If looked for entity is not a wildcard (meaning there are no unknown/
     * unconstrained variables) and this is a redo, nothing more to yield. */
    if (redo && !filter.wildcard) {
        return false;
    }

    int32_t column = -1;
    ecs_table_t *table = NULL;
    ecs_id_record_t *idr;

    if (op->term != -1) {
        columns[op->term] = -1;
    }

    /* If this is a redo, we already looked up the table set */
    if (redo) {
        idr = op_ctx->idr;
    
    /* If this is not a redo lookup the table set. Even though this may not be
     * the first time the operation is evaluated, variables may have changed
     * since last time, which could change the table set to lookup. */
    } else {
        /* Predicates can be reflexive, which means that if we have a
         * transitive predicate which is provided with the same subject and
         * object, it should return true. By default with will not return true
         * as the subject likely does not have itself as a relationship, which
         * is why this is a special case. 
         *
         * TODO: might want to move this code to a separate with_reflexive
         * instruction to limit branches for non-transitive queries (and to keep
         * code more readable).
         */
        if (pair.transitive && pair.reflexive) {
            ecs_entity_t src = 0, second = 0;
            
            if (r == UINT8_MAX) {
                src = op->subject;
            } else {
                const ecs_rule_var_t *v_subj = &rule->vars[r];

                if (v_subj->kind == EcsRuleVarKindEntity) {
                    src = entity_reg_get(rule, regs, r);

                    /* This is the input for the op, so should always be set */
                    ecs_assert(src != 0, ECS_INTERNAL_ERROR, NULL);
                }
            }

            /* If src is set, it means that it is an entity. Try to also 
             * resolve the object. */
            if (src) {
                /* If the object is not a wildcard, it has been reified. Get the
                 * value from either the register or as a literal */
                if (!filter.second_wildcard) {
                    second = ECS_PAIR_SECOND(filter.mask);
                    if (ecs_strip_generation(src) == second) {
                        return true;
                    }
                }
            }
        }

        /* The With operation finds the table set that belongs to its pair
         * filter. The table set is a sparse set that provides an O(1) operation
         * to check whether the current table has the required expression. */
        idr = op_ctx->idr = find_tables(world, filter.mask);
    }

    /* If no table set was found for queried for entity, there are no results. 
     * If this result is a transitive query, the table we're evaluating may not
     * be in the returned table set. Regardless, if the filter that contains a
     * transitive predicate does not have any tables associated with it, there
     * can be no transitive matches for the filter.  */
    if (!idr) {
        return false;
    }

    table = reg_get_range(rule, op, regs, r).table;
    if (!table) {
        return false;
    }

    /* If this is not a redo, start at the beginning */
    if (!redo) {
        column = op_ctx->column = find_next_column(world, table, -1, &filter);
    
    /* If this is a redo, progress to the next match */
    } else {        
        if (!filter.wildcard) {
            return false;
        }
        
        /* Find the next match for the expression in the column. The columns
         * array keeps track of the state for each With operation, so that
         * even after redoing a With, the search doesn't have to start from
         * the beginning. */
        column = find_next_column(world, table, op_ctx->column, &filter);
        op_ctx->column = column;
    }

    /* If no next match was found for this table, no more data */
    if (column == -1) {
        return false;
    }

    if (op->term != -1) {
        columns[op->term] = column;
    }

    /* If we got here, we found a match. Table and column must be set */
    ecs_assert(table != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(column != -1, ECS_INTERNAL_ERROR, NULL);

    /* If this is a wildcard query, fill out the variable registers */
    if (filter.wildcard) {
        reify_variables(iter, op, &filter, table->type, column);
    }

    set_source(it, op, regs, r);

    return true;
}

/* Each operation. The each operation is a simple operation that takes a table
 * as input, and outputs each of the entities in a table. This operation is
 * useful for rules that match a table, and where the entities of the table are
 * used as predicate or object. If a rule contains an each operation, an
 * iterator is guaranteed to yield an entity instead of a table. The input for
 * an each operation can only be the root variable. */
static
bool eval_each(
    ecs_iter_t *it,
    ecs_rule_op_t *op,
    int32_t op_index,
    bool redo)
{
    ecs_rule_iter_t *iter = &it->priv.iter.rule;
    ecs_rule_each_ctx_t *op_ctx = &iter->op_ctx[op_index].is.each;
    ecs_var_t *regs = get_registers(iter, op);
    int32_t r_in = op->r_in;
    int32_t r_out = op->r_out;
    ecs_entity_t e;

    /* Make sure in/out registers are of the correct kind */
    ecs_assert(iter->rule->vars[r_in].kind == EcsRuleVarKindTable, 
        ECS_INTERNAL_ERROR, NULL);
    ecs_assert(iter->rule->vars[r_out].kind == EcsRuleVarKindEntity, 
        ECS_INTERNAL_ERROR, NULL);

    /* Get table, make sure that it contains data. The select operation should
     * ensure that empty tables are never forwarded. */
    ecs_table_range_t slice = table_reg_get(iter->rule, regs, r_in);
    ecs_table_t *table = slice.table;
    if (table) {       
        int32_t row, count = slice.count;
        int32_t offset = slice.offset;

        if (!count) {
            count = ecs_table_count(table);
            ecs_assert(count != 0, ECS_INTERNAL_ERROR, NULL);
        } else {
            count += offset;
        }

        ecs_entity_t *entities = ecs_storage_first(&table->data.entities);
        ecs_assert(entities != NULL, ECS_INTERNAL_ERROR, NULL);

        /* If this is is not a redo, start from row 0, otherwise go to the
        * next entity. */
        if (!redo) {
            row = op_ctx->row = offset;
        } else {
            row = ++ op_ctx->row;
        }

        /* If row exceeds number of entities in table, return false */
        if (row >= count) {
            return false;
        }

        /* Skip builtin entities that could confuse operations */
        e = entities[row];
        while (e == EcsWildcard || e == EcsThis || e == EcsAny) {
            row ++;
            if (row == count) {
                return false;
            }
            e = entities[row];      
        }
    } else {
        if (!redo) {
            e = entity_reg_get(iter->rule, regs, r_in);
        } else {
            return false;
        }
    }

    /* Assign entity */
    entity_reg_set(iter->rule, regs, r_out, e);

    return true;
}

/* Store operation. Stores entity in register. This can either be an entity 
 * literal or an entity variable that will be stored in a table register. The
 * latter facilitates scenarios where an iterator only need to return a single
 * entity but where the Yield returns tables. */
static
bool eval_store(
    ecs_iter_t *it,
    ecs_rule_op_t *op,
    int32_t op_index,
    bool redo)
{
    (void)op_index;

    if (redo) {
        /* Only ever return result once */
        return false;
    }

    ecs_rule_iter_t *iter = &it->priv.iter.rule;
    const ecs_rule_t *rule = iter->rule;
    ecs_world_t *world = rule->world;
    ecs_var_t *regs = get_registers(iter, op);
    int32_t r_in = op->r_in;
    int32_t r_out = op->r_out;

    (void)world;

    const ecs_rule_var_t *var_out = &rule->vars[r_out];
    if (var_out->kind == EcsRuleVarKindEntity) {
        ecs_entity_t out, in = reg_get_entity(rule, op, regs, r_in);
        ecs_assert(in != 0, ECS_INTERNAL_ERROR, NULL);
        ecs_assert(ecs_is_valid(world, in), ECS_INTERNAL_ERROR, NULL);

        out = iter->registers[r_out].entity;
        bool output_is_input = ecs_iter_var_is_constrained(it, r_out);
        if (output_is_input && !redo) {
            ecs_assert(regs[r_out].entity == iter->registers[r_out].entity, 
                ECS_INTERNAL_ERROR, NULL);

            if (out != in) {
                /* If output variable is set it must match the input */
                return false;
            }
        }

        reg_set_entity(rule, regs, r_out, in);
    } else {
        ecs_table_range_t out, in = reg_get_range(rule, op, regs, r_in);

        out = iter->registers[r_out].range;
        bool output_is_input = out.table != NULL;

        if (output_is_input && !redo) {
            ecs_assert(regs[r_out].entity == iter->registers[r_out].entity, 
                ECS_INTERNAL_ERROR, NULL);

            if (ecs_os_memcmp_t(&out, &in, ecs_table_range_t)) {
                /* If output variable is set it must match the input */
                return false;
            }
        }

        reg_set_range(rule, regs, r_out, &in);

        /* Ensure that if the input was an empty entity, information is not
         * lost */
        if (!regs[r_out].range.table) {
            regs[r_out].entity = reg_get_entity(rule, op, regs, r_in);
            ecs_assert(ecs_is_valid(world, regs[r_out].entity), 
                ECS_INTERNAL_ERROR, NULL);
        }
    }

    ecs_rule_filter_t filter = pair_to_filter(iter, op, op->filter);
    set_term_vars(rule, regs, op->term, filter.mask);

    return true;
}

/* A setjmp operation sets the jump label for a subsequent jump label. When the
 * operation is first evaluated (redo=false) it sets the label to the on_pass
 * label, and returns true. When the operation is evaluated again (redo=true)
 * the label is set to on_fail and the operation returns false. */
static
bool eval_setjmp(
    ecs_iter_t *it,
    ecs_rule_op_t *op,
    int32_t op_index,
    bool redo)
{
    ecs_rule_iter_t *iter = &it->priv.iter.rule;
    ecs_rule_setjmp_ctx_t *ctx = &iter->op_ctx[op_index].is.setjmp;

    if (!redo) {
        ctx->label = op->on_pass;
        return true;
    } else {
        ctx->label = op->on_fail;
        return false;
    }
}

/* The jump operation jumps to an operation label. The operation always returns
 * true. Since the operation modifies the control flow of the program directly, 
 * the dispatcher does not look at the on_pass or on_fail labels of the jump
 * instruction. Instead, the on_pass label is used to store the label of the
 * operation that contains the label to jump to. */
static
bool eval_jump(
    ecs_iter_t *it,
    ecs_rule_op_t *op,
    int32_t op_index,
    bool redo)
{
    (void)it;
    (void)op;
    (void)op_index;

    /* Passthrough, result is not used for control flow */
    return !redo;
}

/* The not operation reverts the result of the operation it embeds */
static
bool eval_not(
    ecs_iter_t *it,
    ecs_rule_op_t *op,
    int32_t op_index,
    bool redo)
{
    (void)it;
    (void)op;
    (void)op_index;

    return !redo;
}

/* Check if entity is stored in table */
static
bool eval_intable(
    ecs_iter_t *it,
    ecs_rule_op_t *op,
    int32_t op_index,
    bool redo)
{
    (void)op_index;
    
    if (redo) {
        return false;
    }

    ecs_rule_iter_t *iter = &it->priv.iter.rule;
    const ecs_rule_t  *rule = iter->rule;
    ecs_world_t *world = rule->world;
    ecs_var_t *regs = get_registers(iter, op);
    ecs_table_t *table = table_reg_get(rule, regs, op->r_in).table;

    ecs_rule_pair_t pair = op->filter;
    ecs_rule_filter_t filter = pair_to_filter(iter, op, pair);
    ecs_entity_t second = ECS_PAIR_SECOND(filter.mask);
    ecs_assert(second != 0 && second != EcsWildcard, ECS_INTERNAL_ERROR, NULL);
    second = ecs_get_alive(world, second);
    ecs_assert(second != 0, ECS_INTERNAL_ERROR, NULL);

    ecs_table_t *obj_table = ecs_get_table(world, second);
    return obj_table == table;
}

/* Yield operation. This is the simplest operation, as all it does is return
 * false. This will move the solver back to the previous instruction which
 * forces redo's on previous operations, for as long as there are matching
 * results. */
static
bool eval_yield(
    ecs_iter_t *it,
    ecs_rule_op_t *op,
    int32_t op_index,
    bool redo)
{
    (void)it;
    (void)op;
    (void)op_index;
    (void)redo;

    /* Yield always returns false, because there are never any operations after
     * a yield. */
    return false;
}

/* Dispatcher for operations */
static
bool eval_op(
    ecs_iter_t *it,
    ecs_rule_op_t *op,
    int32_t op_index,
    bool redo)
{
    switch(op->kind) {
    case EcsRuleInput:
        return eval_input(it, op, op_index, redo);
    case EcsRuleSelect:
        return eval_select(it, op, op_index, redo);
    case EcsRuleWith:
        return eval_with(it, op, op_index, redo);
    case EcsRuleSubSet:
        return eval_subset(it, op, op_index, redo);
    case EcsRuleSuperSet:
        return eval_superset(it, op, op_index, redo);
    case EcsRuleEach:
        return eval_each(it, op, op_index, redo);
    case EcsRuleStore:
        return eval_store(it, op, op_index, redo);
    case EcsRuleSetJmp:
        return eval_setjmp(it, op, op_index, redo);
    case EcsRuleJump:
        return eval_jump(it, op, op_index, redo);
    case EcsRuleNot:
        return eval_not(it, op, op_index, redo);
    case EcsRuleInTable:
        return eval_intable(it, op, op_index, redo);
    case EcsRuleYield:
        return eval_yield(it, op, op_index, redo);
    default:
        return false;
    }
}

/* Utility to copy all registers to the next frame. Keeping track of register
 * values for each operation is necessary, because if an operation is asked to
 * redo matching, it must to be able to pick up from where it left of */
static
void push_registers(
    ecs_rule_iter_t *it,
    int32_t cur,
    int32_t next)
{
    if (!it->rule->var_count) {
        return;
    }

    ecs_var_t *src_regs = get_register_frame(it, cur);
    ecs_var_t *dst_regs = get_register_frame(it, next);

    ecs_os_memcpy_n(dst_regs, src_regs, 
        ecs_var_t, it->rule->var_count);
}

/* Utility to copy all columns to the next frame. Columns keep track of which
 * columns are currently being evaluated for a table, and are populated by the
 * Select and With operations. The columns array is important, as it is used
 * to tell the application where to find component data. */
static
void push_columns(
    ecs_rule_iter_t *it,
    int32_t cur,
    int32_t next)
{
    if (!it->rule->filter.term_count) {
        return;
    }

    int32_t *src_cols = rule_get_columns_frame(it, cur);
    int32_t *dst_cols = rule_get_columns_frame(it, next);

    ecs_os_memcpy_n(dst_cols, src_cols, int32_t, it->rule->filter.term_count);
}

/* Populate iterator with data before yielding to application */
static
void populate_iterator(
    const ecs_rule_t *rule,
    ecs_iter_t *iter,
    ecs_rule_iter_t *it,
    ecs_rule_op_t *op)
{
    ecs_world_t *world = rule->world;
    int32_t r = op->r_in;
    ecs_var_t *regs = get_register_frame(it, op->frame);
    ecs_table_t *table = NULL;
    int32_t count = 0;
    int32_t offset = 0;

    /* If the input register for the yield does not point to a variable,
     * the rule doesn't contain a this (.) variable. In that case, the
     * iterator doesn't contain any data, and this function will simply
     * return true or false. An application will still be able to obtain
     * the variables that were resolved. */
    if (r != UINT8_MAX) {
        const ecs_rule_var_t *var = &rule->vars[r];
        ecs_var_t *reg = &regs[r];

        if (var->kind == EcsRuleVarKindTable) {
            ecs_table_range_t slice = table_reg_get(rule, regs, r);
            table = slice.table;
            count = slice.count;
            offset = slice.offset;
        } else {
            /* If a single entity is returned, simply return the
             * iterator with count 1 and a pointer to the entity id */
            ecs_assert(var->kind == EcsRuleVarKindEntity, 
                ECS_INTERNAL_ERROR, NULL);

            ecs_entity_t e = reg->entity;
            ecs_assert(ecs_is_valid(world, e), ECS_INTERNAL_ERROR, NULL);
            ecs_record_t *record = flecs_entities_get(world, e);
            offset = ECS_RECORD_TO_ROW(record->row);

            /* If an entity is not stored in a table, it could not have
             * been matched by anything */
            ecs_assert(record != NULL, ECS_INTERNAL_ERROR, NULL);
            table = record->table;
            count = 1;
        }
    }

    int32_t i, var_count = rule->var_count;
    int32_t term_count = rule->filter.term_count;

    for (i = 0; i < var_count; i ++) {
        iter->variables[i] = regs[i];
    }

    for (i = 0; i < term_count; i ++) {
        int32_t v = rule->term_vars[i].src;
        if (v != -1) {
            const ecs_rule_var_t *var = &rule->vars[v];
            if (var->name[0] != '.') {
                if (var->kind == EcsRuleVarKindEntity) {
                    iter->sources[i] = regs[var->id].entity;
                } else {
                    /* This can happen for Any variables, where the actual
                     * content of the variable is not of interest to the query.
                     * Just pick the first entity from the table, so that the 
                     * column can be correctly resolved */
                    ecs_table_t *t = regs[var->id].range.table;
                    if (t) {
                        iter->sources[i] = ecs_storage_first_t(
                            &t->data.entities, ecs_entity_t)[0];
                    } else {
                        /* Can happen if term is optional */
                        iter->sources[i] = 0;
                    }
                }
            }
        }
    }

    /* Iterator expects column indices to start at 1 */
    iter->columns = rule_get_columns_frame(it, op->frame);
    for (i = 0; i < term_count; i ++) {
        ecs_assert(iter->sources != NULL, ECS_INTERNAL_ERROR, NULL);
        ecs_entity_t src = iter->sources[i];
        int32_t c = ++ iter->columns[i];
        if (!src) {
            src = iter->terms[i].src.id;
            if (src != EcsThis && src != EcsAny) {
                iter->columns[i] = 0;
            }
        } else if (c) {
            iter->columns[i] = -1;
        }
    }

    /* Set iterator ids */
    for (i = 0; i < term_count; i ++) {
        const ecs_rule_term_vars_t *vars = &rule->term_vars[i];
        ecs_term_t *term = &rule->filter.terms[i];
        if (term->oper == EcsOptional || term->oper == EcsNot) {
            if (iter->columns[i] == 0) {
                iter->ids[i] = term->id;
                continue;
            }
        }

        ecs_id_t id = term->id;
        ecs_entity_t first = 0;
        ecs_entity_t second = 0;
        bool is_pair = ECS_HAS_ROLE(id, PAIR);

        if (!is_pair) {
            first = id;
        } else {
            first = ECS_PAIR_FIRST(id);
            second = ECS_PAIR_SECOND(id);
        }

        if (vars->first != -1) {
            first = regs[vars->first].entity;
        }
        if (vars->second != -1) {
            ecs_assert(is_pair, ECS_INTERNAL_ERROR, NULL);
            second = regs[vars->second].entity;
        }

        if (!is_pair) {
            id = first;
        } else {
            id = ecs_pair(first, second);
        }

        iter->ids[i] = id;
    }

    flecs_iter_populate_data(world, iter, table, offset, count, 
        iter->ptrs, iter->sizes);
}

static
bool is_control_flow(
    ecs_rule_op_t *op)
{
    switch(op->kind) {
    case EcsRuleSetJmp:
    case EcsRuleJump:
        return true;
    default:
        return false;
    }
}

static
void rule_iter_set_initial_state(
    ecs_iter_t *it,
    ecs_rule_iter_t *iter,
    const ecs_rule_t *rule)
{
    int32_t i;

    /* Make sure that if there are any terms with literal sources, they're
     * initialized in the sources array */
    const ecs_filter_t *filter = &rule->filter;
    int32_t term_count = filter->term_count;
    for (i = 0; i < term_count; i ++) {
        ecs_term_t *t = &filter->terms[i];
        ecs_term_id_t *src = &t->src;
        ecs_assert(src->flags & EcsIsVariable || src->id != EcsThis,
            ECS_INTERNAL_ERROR, NULL);

        if (!(src->flags & EcsIsVariable)) {
            it->sources[i] = src->id;
        }
    }

    /* Initialize registers of constrained variables */
    if (it->constrained_vars) {
        ecs_var_t *regs = get_register_frame(iter, 0);

        for (i = 0; i < it->variable_count; i ++) {
            if (ecs_iter_var_is_constrained(it, i)) {
                const ecs_rule_var_t *var = &rule->vars[i];
                ecs_assert(var->id == i, ECS_INTERNAL_ERROR, NULL);
                
                int32_t other_var = var->other;
                ecs_var_t *it_var = &it->variables[i];
                ecs_entity_t e = it_var->entity;

                if (e) {
                    ecs_assert(ecs_is_valid(it->world, e), 
                        ECS_INTERNAL_ERROR, NULL);
                    reg_set_entity(rule, regs, i, e);
                    if (other_var != -1) {
                        reg_set_entity(rule, regs, other_var, e);
                    }
                } else {
                    ecs_assert(it_var->range.table != NULL, 
                        ECS_INVALID_PARAMETER, NULL);
                    reg_set_range(rule, regs, i, &it_var->range);
                    if (other_var != -1) {
                        reg_set_range(rule, regs, other_var, &it_var->range);
                    }
                }
            }
        }
    }
}

bool ecs_rule_next(
    ecs_iter_t *it)
{
    ecs_check(it != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(it->next == ecs_rule_next, ECS_INVALID_PARAMETER, NULL);

    if (flecs_iter_next_row(it)) {
        return true;
    }

    return flecs_iter_next_instanced(it, ecs_rule_next_instanced(it));
error:
    return false;
}

/* Iterator next function. This evaluates the program until it reaches a Yield
 * operation, and returns the intermediate result(s) to the application. An
 * iterator can, depending on the program, either return a table, entity, or
 * just true/false, in case a rule doesn't contain the this variable. */
bool ecs_rule_next_instanced(
    ecs_iter_t *it)
{
    ecs_check(it != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(it->next == ecs_rule_next, ECS_INVALID_PARAMETER, NULL);

    ecs_rule_iter_t *iter = &it->priv.iter.rule;
    const ecs_rule_t *rule = iter->rule;
    bool redo = iter->redo;
    int32_t last_frame = -1;
    bool first_time = !ECS_BIT_IS_SET(it->flags, EcsIterIsValid);

    ecs_poly_assert(rule, ecs_rule_t);

    /* Mark iterator as valid & ensure iterator resources are up to date */
    flecs_iter_validate(it);

    /* Can't iterate an iterator that's already depleted */
    ecs_check(iter->op != -1, ECS_INVALID_PARAMETER, NULL);

    /* If this is the first time the iterator is iterated, set initial state */
    if (first_time) {
        ecs_assert(redo == false, ECS_INTERNAL_ERROR, NULL);
        rule_iter_set_initial_state(it, iter, rule);
    }

    do {
        /* Evaluate an operation. The result of an operation determines the
         * flow of the program. If an operation returns true, the program 
         * continues to the operation pointed to by 'on_pass'. If the operation
         * returns false, the program continues to the operation pointed to by
         * 'on_fail'.
         *
         * In most scenarios, on_pass points to the next operation, and on_fail
         * points to the previous operation.
         *
         * When an operation fails, the previous operation will be invoked with
         * redo=true. This will cause the operation to continue its search from
         * where it left off. When the operation succeeds, the next operation
         * will be invoked with redo=false. This causes the operation to start
         * from the beginning, which is necessary since it just received a new
         * input. */
        int32_t op_index = iter->op;
        ecs_rule_op_t *op = &rule->operations[op_index];
        int32_t cur = op->frame;

        /* If this is not the first operation and is also not a control flow
         * operation, push a new frame on the stack for the next operation */
        if (!redo && !is_control_flow(op) && cur && cur != last_frame) {
            int32_t prev = cur - 1;
            push_registers(iter, prev, cur);
            push_columns(iter, prev, cur);
        }

        /* Dispatch the operation */
        bool result = eval_op(it, op, op_index, redo);
        iter->op = result ? op->on_pass : op->on_fail;

        /* If the current operation is yield, return results */
        if (op->kind == EcsRuleYield) {
            populate_iterator(rule, it, iter, op);
            iter->redo = true;
            return true;
        }

        /* If the current operation is a jump, goto stored label */
        if (op->kind == EcsRuleJump) {
            /* Label is stored in setjmp context */
            iter->op = iter->op_ctx[op->on_pass].is.setjmp.label;
        }

        /* If jumping backwards, it's a redo */
        redo = iter->op <= op_index;

        if (!is_control_flow(op)) {
            last_frame = op->frame;
        }
    } while (iter->op != -1);

    ecs_iter_fini(it);

error:
    return false;
}

#endif


#ifdef FLECS_MODULE

#include <ctype.h>

char* ecs_module_path_from_c(
    const char *c_name)
{
    ecs_strbuf_t str = ECS_STRBUF_INIT;
    const char *ptr;
    char ch;

    for (ptr = c_name; (ch = *ptr); ptr++) {
        if (isupper(ch)) {
            ch = flecs_ito(char, tolower(ch));
            if (ptr != c_name) {
                ecs_strbuf_appendstrn(&str, ".", 1);
            }
        }

        ecs_strbuf_appendstrn(&str, &ch, 1);
    }

    return ecs_strbuf_get(&str);
}

ecs_entity_t ecs_import(
    ecs_world_t *world,
    ecs_module_action_t module,
    const char *module_name)
{
    ecs_check(!(world->flags & EcsWorldReadonly), 
        ECS_INVALID_WHILE_READONLY, NULL);

    ecs_entity_t old_scope = ecs_set_scope(world, 0);
    const char *old_name_prefix = world->name_prefix;

    char *path = ecs_module_path_from_c(module_name);
    ecs_entity_t e = ecs_lookup_fullpath(world, path);
    ecs_os_free(path);
    
    if (!e) {
        ecs_trace("#[magenta]import#[reset] %s", module_name);
        ecs_log_push();

        /* Load module */
        module(world);

        /* Lookup module entity (must be registered by module) */
        e = ecs_lookup_fullpath(world, module_name);
        ecs_check(e != 0, ECS_MODULE_UNDEFINED, module_name);

        ecs_log_pop();
    }

    /* Restore to previous state */
    ecs_set_scope(world, old_scope);
    world->name_prefix = old_name_prefix;

    return e;
error:
    return 0;
}

ecs_entity_t ecs_import_c(
    ecs_world_t *world,
    ecs_module_action_t module,
    const char *c_name)
{
    char *name = ecs_module_path_from_c(c_name);
    ecs_entity_t e = ecs_import(world, module, name);
    ecs_os_free(name);
    return e;
}

ecs_entity_t ecs_import_from_library(
    ecs_world_t *world,
    const char *library_name,
    const char *module_name)
{
    ecs_check(library_name != NULL, ECS_INVALID_PARAMETER, NULL);

    char *import_func = (char*)module_name; /* safe */
    char *module = (char*)module_name;

    if (!ecs_os_has_modules() || !ecs_os_has_dl()) {
        ecs_err(
            "library loading not supported, set module_to_dl, dlopen, dlclose "
            "and dlproc os API callbacks first");
        return 0;
    }

    /* If no module name is specified, try default naming convention for loading
     * the main module from the library */
    if (!import_func) {
        import_func = ecs_os_malloc(ecs_os_strlen(library_name) + ECS_SIZEOF("Import"));
        ecs_assert(import_func != NULL, ECS_OUT_OF_MEMORY, NULL);
        
        const char *ptr;
        char ch, *bptr = import_func;
        bool capitalize = true;
        for (ptr = library_name; (ch = *ptr); ptr ++) {
            if (ch == '.') {
                capitalize = true;
            } else {
                if (capitalize) {
                    *bptr = flecs_ito(char, toupper(ch));
                    bptr ++;
                    capitalize = false;
                } else {
                    *bptr = flecs_ito(char, tolower(ch));
                    bptr ++;
                }
            }
        }

        *bptr = '\0';

        module = ecs_os_strdup(import_func);
        ecs_assert(module != NULL, ECS_OUT_OF_MEMORY, NULL);

        ecs_os_strcat(bptr, "Import");
    }

    char *library_filename = ecs_os_module_to_dl(library_name);
    if (!library_filename) {
        ecs_err("failed to find library file for '%s'", library_name);
        if (module != module_name) {
            ecs_os_free(module);
        }
        return 0;
    } else {
        ecs_trace("found file '%s' for library '%s'", 
            library_filename, library_name);
    }

    ecs_os_dl_t dl = ecs_os_dlopen(library_filename);
    if (!dl) {
        ecs_err("failed to load library '%s' ('%s')", 
            library_name, library_filename);
        
        ecs_os_free(library_filename);

        if (module != module_name) {
            ecs_os_free(module);
        }    

        return 0;
    } else {
        ecs_trace("library '%s' ('%s') loaded", 
            library_name, library_filename);
    }

    ecs_module_action_t action = (ecs_module_action_t)
        ecs_os_dlproc(dl, import_func);
    if (!action) {
        ecs_err("failed to load import function %s from library %s",
            import_func, library_name);
        ecs_os_free(library_filename);
        ecs_os_dlclose(dl);            
        return 0;
    } else {
        ecs_trace("found import function '%s' in library '%s' for module '%s'",
            import_func, library_name, module);
    }

    /* Do not free id, as it will be stored as the component identifier */
    ecs_entity_t result = ecs_import(world, action, module);

    if (import_func != module_name) {
        ecs_os_free(import_func);
    }

    if (module != module_name) {
        ecs_os_free(module);
    }

    ecs_os_free(library_filename);

    return result;
error:
    return 0;
}

ecs_entity_t ecs_module_init(
    ecs_world_t *world,
    const char *c_name,
    const ecs_component_desc_t *desc)
{
    ecs_check(desc != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_poly_assert(world, ecs_world_t);

    ecs_entity_t e = desc->entity;
    if (!e) {
        char *module_path = ecs_module_path_from_c(c_name);
        e = ecs_new_from_fullpath(world, module_path);
        ecs_set_symbol(world, e, module_path);
        ecs_os_free(module_path);
    }
    
    ecs_add_id(world, e, EcsModule);

    ecs_component_desc_t private_desc = *desc;
    private_desc.entity = e;

    if (desc->type.size) {
        ecs_entity_t result = ecs_component_init(world, &private_desc);
        ecs_assert(result != 0, ECS_INTERNAL_ERROR, NULL);
        ecs_assert(result == e, ECS_INTERNAL_ERROR, NULL);
        (void)result;
    }

    return e;
error:
    return 0;
}

#endif

#ifndef FLECS_META_PRIVATE_H
#define FLECS_META_PRIVATE_H


#ifdef FLECS_META

void ecs_meta_type_serialized_init(
    ecs_iter_t *it);

void ecs_meta_dtor_serialized(
    EcsMetaTypeSerialized *ptr);


bool flecs_unit_validate(
    ecs_world_t *world,
    ecs_entity_t t,
    EcsUnit *data);

#endif
    
#endif


#ifdef FLECS_META

ecs_entity_t ecs_primitive_init(
    ecs_world_t *world,
    const ecs_primitive_desc_t *desc)
{
    ecs_entity_t t = desc->entity;
    if (!t) {
        t = ecs_new_low_id(world);
    }

    ecs_set(world, t, EcsPrimitive, { desc->kind });

    return t;
}

ecs_entity_t ecs_enum_init(
    ecs_world_t *world,
    const ecs_enum_desc_t *desc)
{
    ecs_entity_t t = desc->entity;
    if (!t) {
        t = ecs_new_low_id(world);
    }

    ecs_add(world, t, EcsEnum);

    ecs_entity_t old_scope = ecs_set_scope(world, t);

    int i;
    for (i = 0; i < ECS_MEMBER_DESC_CACHE_SIZE; i ++) {
        const ecs_enum_constant_t *m_desc = &desc->constants[i];
        if (!m_desc->name) {
            break;
        }

        ecs_entity_t c = ecs_entity_init(world, &(ecs_entity_desc_t){
            .name = m_desc->name
        });

        if (!m_desc->value) {
            ecs_add_id(world, c, EcsConstant);
        } else {
            ecs_set_pair_object(world, c, EcsConstant, ecs_i32_t, 
                {m_desc->value});
        }
    }

    ecs_set_scope(world, old_scope);

    if (i == 0) {
        ecs_err("enum '%s' has no constants", ecs_get_name(world, t));
        ecs_delete(world, t);
        return 0;
    }

    return t;
}

ecs_entity_t ecs_bitmask_init(
    ecs_world_t *world,
    const ecs_bitmask_desc_t *desc)
{
    ecs_entity_t t = desc->entity;
    if (!t) {
        t = ecs_new_low_id(world);
    }

    ecs_add(world, t, EcsBitmask);

    ecs_entity_t old_scope = ecs_set_scope(world, t);

    int i;
    for (i = 0; i < ECS_MEMBER_DESC_CACHE_SIZE; i ++) {
        const ecs_bitmask_constant_t *m_desc = &desc->constants[i];
        if (!m_desc->name) {
            break;
        }

        ecs_entity_t c = ecs_entity_init(world, &(ecs_entity_desc_t){
            .name = m_desc->name
        });

        if (!m_desc->value) {
            ecs_add_id(world, c, EcsConstant);
        } else {
            ecs_set_pair_object(world, c, EcsConstant, ecs_u32_t, 
                {m_desc->value});
        }
    }

    ecs_set_scope(world, old_scope);

    if (i == 0) {
        ecs_err("bitmask '%s' has no constants", ecs_get_name(world, t));
        ecs_delete(world, t);
        return 0;
    }

    return t;
}

ecs_entity_t ecs_array_init(
    ecs_world_t *world,
    const ecs_array_desc_t *desc)
{
    ecs_entity_t t = desc->entity;
    if (!t) {
        t = ecs_new_low_id(world);
    }

    ecs_set(world, t, EcsArray, {
        .type = desc->type,
        .count = desc->count
    });

    return t;
}

ecs_entity_t ecs_vector_init(
    ecs_world_t *world,
    const ecs_vector_desc_t *desc)
{
    ecs_entity_t t = desc->entity;
    if (!t) {
        t = ecs_new_low_id(world);
    }

    ecs_set(world, t, EcsVector, {
        .type = desc->type
    });

    return t;
}

ecs_entity_t ecs_struct_init(
    ecs_world_t *world,
    const ecs_struct_desc_t *desc)
{
    ecs_entity_t t = desc->entity;
    if (!t) {
        t = ecs_new_low_id(world);
    }

    ecs_entity_t old_scope = ecs_set_scope(world, t);

    int i;
    for (i = 0; i < ECS_MEMBER_DESC_CACHE_SIZE; i ++) {
        const ecs_member_t *m_desc = &desc->members[i];
        if (!m_desc->type) {
            break;
        }

        if (!m_desc->name) {
            ecs_err("member %d of struct '%s' does not have a name", i, 
                ecs_get_name(world, t));
            ecs_delete(world, t);
            return 0;
        }

        ecs_entity_t m = ecs_entity_init(world, &(ecs_entity_desc_t){
            .name = m_desc->name
        });

        ecs_set(world, m, EcsMember, {
            .type = m_desc->type, 
            .count = m_desc->count,
            .offset = m_desc->offset,
            .unit = m_desc->unit
        });
    }

    ecs_set_scope(world, old_scope);

    if (i == 0) {
        ecs_err("struct '%s' has no members", ecs_get_name(world, t));
        ecs_delete(world, t);
        return 0;
    }

    if (!ecs_has(world, t, EcsStruct)) {
        /* Invalid members */
        ecs_delete(world, t);
        return 0;
    }

    return t;
}

ecs_entity_t ecs_unit_init(
    ecs_world_t *world,
    const ecs_unit_desc_t *desc)
{
    ecs_entity_t t = desc->entity;
    if (!t) {
        t = ecs_new_low_id(world);
    }

    ecs_entity_t quantity = desc->quantity;
    if (quantity) {
        if (!ecs_has_id(world, quantity, EcsQuantity)) {
            ecs_err("entity '%s' for unit '%s' is not a quantity",
                ecs_get_name(world, quantity), ecs_get_name(world, t));
            goto error;
        }

        ecs_add_pair(world, t, EcsQuantity, desc->quantity);
    } else {
        ecs_remove_pair(world, t, EcsQuantity, EcsWildcard);
    }

    EcsUnit *value = ecs_get_mut(world, t, EcsUnit);
    value->base = desc->base;
    value->over = desc->over;
    value->translation = desc->translation;
    value->prefix = desc->prefix;
    ecs_os_strset(&value->symbol, desc->symbol);

    if (!flecs_unit_validate(world, t, value)) {
        goto error;
    }

    ecs_modified(world, t, EcsUnit);

    return t;
error:
    if (t) {
        ecs_delete(world, t);
    }
    return 0;
}

ecs_entity_t ecs_unit_prefix_init(
    ecs_world_t *world,
    const ecs_unit_prefix_desc_t *desc)
{
    ecs_entity_t t = desc->entity;
    if (!t) {
        t = ecs_new_low_id(world);
    }

    ecs_set(world, t, EcsUnitPrefix, {
        .symbol = (char*)desc->symbol,
        .translation = desc->translation
    });

    return t;
}

ecs_entity_t ecs_quantity_init(
    ecs_world_t *world,
    const ecs_entity_desc_t *desc)
{
    ecs_entity_t t = ecs_entity_init(world, desc);
    if (!t) {
        return 0;
    }

    ecs_add_id(world, t, EcsQuantity);

    return t;
}

#endif


#ifdef FLECS_META

static
ecs_vector_t* serialize_type(
    ecs_world_t *world,
    ecs_entity_t type,
    ecs_size_t offset,
    ecs_vector_t *ops);

static
ecs_meta_type_op_kind_t primitive_to_op_kind(ecs_primitive_kind_t kind) {
    return EcsOpPrimitive + kind;
}

static
ecs_size_t type_size(ecs_world_t *world, ecs_entity_t type) {
    const EcsComponent *comp = ecs_get(world, type, EcsComponent);
    ecs_assert(comp != NULL, ECS_INTERNAL_ERROR, NULL);
    return comp->size;
}

static
ecs_meta_type_op_t* ops_add(ecs_vector_t **ops, ecs_meta_type_op_kind_t kind) {
    ecs_meta_type_op_t *op = ecs_vector_add(ops, ecs_meta_type_op_t);
    op->kind = kind;
    op->offset = 0;
    op->count = 1;
    op->op_count = 1;
    op->size = 0;
    op->name = NULL;
    op->members = NULL;
    op->type = 0;
    op->unit = 0;
    return op;
}

static
ecs_meta_type_op_t* ops_get(ecs_vector_t *ops, int32_t index) {
    ecs_meta_type_op_t* op = ecs_vector_get(ops, ecs_meta_type_op_t, index);
    ecs_assert(op != NULL, ECS_INTERNAL_ERROR, NULL);
    return op;
}

static
ecs_vector_t* serialize_primitive(
    ecs_world_t *world,
    ecs_entity_t type,
    ecs_size_t offset,
    ecs_vector_t *ops)
{
    const EcsPrimitive *ptr = ecs_get(world, type, EcsPrimitive);
    ecs_assert(ptr != NULL, ECS_INTERNAL_ERROR, NULL);

    ecs_meta_type_op_t *op = ops_add(&ops, primitive_to_op_kind(ptr->kind));
    op->offset = offset,
    op->type = type;
    op->size = type_size(world, type);

    return ops;
}

static
ecs_vector_t* serialize_enum(
    ecs_world_t *world,
    ecs_entity_t type,
    ecs_size_t offset,
    ecs_vector_t *ops)
{
    (void)world;
    
    ecs_meta_type_op_t *op = ops_add(&ops, EcsOpEnum);
    op->offset = offset,
    op->type = type;
    op->size = ECS_SIZEOF(ecs_i32_t);

    return ops;
}

static
ecs_vector_t* serialize_bitmask(
    ecs_world_t *world,
    ecs_entity_t type,
    ecs_size_t offset,
    ecs_vector_t *ops)
{
    (void)world;
    
    ecs_meta_type_op_t *op = ops_add(&ops, EcsOpBitmask);
    op->offset = offset,
    op->type = type;
    op->size = ECS_SIZEOF(ecs_u32_t);

    return ops;
}

static
ecs_vector_t* serialize_array(
    ecs_world_t *world,
    ecs_entity_t type,
    ecs_size_t offset,
    ecs_vector_t *ops)
{
    (void)world;

    ecs_meta_type_op_t *op = ops_add(&ops, EcsOpArray);
    op->offset = offset;
    op->type = type;
    op->size = type_size(world, type);

    return ops;
}

static
ecs_vector_t* serialize_array_component(
    ecs_world_t *world,
    ecs_entity_t type)
{
    const EcsArray *ptr = ecs_get(world, type, EcsArray);
    if (!ptr) {
        return NULL; /* Should never happen, will trigger internal error */
    }

    ecs_vector_t *ops = serialize_type(world, ptr->type, 0, NULL);
    ecs_assert(ops != NULL, ECS_INTERNAL_ERROR, NULL);

    ecs_meta_type_op_t *first = ecs_vector_first(ops, ecs_meta_type_op_t);
    first->count = ptr->count;

    return ops;
}

static
ecs_vector_t* serialize_vector(
    ecs_world_t *world,
    ecs_entity_t type,
    ecs_size_t offset,
    ecs_vector_t *ops)
{
    (void)world;

    ecs_meta_type_op_t *op = ops_add(&ops, EcsOpVector);
    op->offset = offset;
    op->type = type;
    op->size = type_size(world, type);

    return ops;
}

static
ecs_vector_t* serialize_struct(
    ecs_world_t *world,
    ecs_entity_t type,
    ecs_size_t offset,
    ecs_vector_t *ops)
{
    const EcsStruct *ptr = ecs_get(world, type, EcsStruct);
    ecs_assert(ptr != NULL, ECS_INTERNAL_ERROR, NULL);

    int32_t cur, first = ecs_vector_count(ops);
    ecs_meta_type_op_t *op = ops_add(&ops, EcsOpPush);
    op->offset = offset;
    op->type = type;
    op->size = type_size(world, type);

    ecs_member_t *members = ecs_vector_first(ptr->members, ecs_member_t);
    int32_t i, count = ecs_vector_count(ptr->members);

    ecs_hashmap_t *member_index = NULL;
    if (count) {        
        op->members = member_index = flecs_name_index_new();
    }

    for (i = 0; i < count; i ++) {
        ecs_member_t *member = &members[i];

        cur = ecs_vector_count(ops);
        ops = serialize_type(world, member->type, offset + member->offset, ops);

        op = ops_get(ops, cur);
        if (!op->type) {
            op->type = member->type;
        }

        if (op->count <= 1) {
            op->count = member->count;
        }

        const char *member_name = member->name;
        op->name = member_name;
        op->unit = member->unit;
        op->op_count = ecs_vector_count(ops) - cur;

        flecs_name_index_ensure(
            member_index, flecs_ito(uint64_t, cur - first - 1), 
                member_name, 0, 0);
    }

    ops_add(&ops, EcsOpPop);
    ops_get(ops, first)->op_count = ecs_vector_count(ops) - first;

    return ops;
}

static
ecs_vector_t* serialize_type(
    ecs_world_t *world,
    ecs_entity_t type,
    ecs_size_t offset,
    ecs_vector_t *ops)
{
    const EcsMetaType *ptr = ecs_get(world, type, EcsMetaType);
    if (!ptr) {
        char *path = ecs_get_fullpath(world, type);
        ecs_err("missing EcsMetaType for type %s'", path);
        ecs_os_free(path);
        return NULL;
    }

    switch(ptr->kind) {
    case EcsPrimitiveType:
        ops = serialize_primitive(world, type, offset, ops);
        break;

    case EcsEnumType:
        ops = serialize_enum(world, type, offset, ops);
        break;

    case EcsBitmaskType:
        ops = serialize_bitmask(world, type, offset, ops);
        break;

    case EcsStructType:
        ops = serialize_struct(world, type, offset, ops);
        break;

    case EcsArrayType:
        ops = serialize_array(world, type, offset, ops);
        break;

    case EcsVectorType:
        ops = serialize_vector(world, type, offset, ops);
        break;
    }

    return ops;
}

static
ecs_vector_t* serialize_component(
    ecs_world_t *world,
    ecs_entity_t type)
{
    const EcsMetaType *ptr = ecs_get(world, type, EcsMetaType);
    if (!ptr) {
        char *path = ecs_get_fullpath(world, type);
        ecs_err("missing EcsMetaType for type %s'", path);
        ecs_os_free(path);
        return NULL;
    }

    ecs_vector_t *ops = NULL;

    switch(ptr->kind) {
    case EcsArrayType:
        ops = serialize_array_component(world, type);
        break;
    default:
        ops = serialize_type(world, type, 0, NULL);
        break;
    }

    return ops;
}

void ecs_meta_type_serialized_init(
    ecs_iter_t *it)
{
    ecs_world_t *world = it->world;

    int i, count = it->count;
    for (i = 0; i < count; i ++) {
        ecs_entity_t e = it->entities[i];
        ecs_vector_t *ops = serialize_component(world, e);
        ecs_assert(ops != NULL, ECS_INTERNAL_ERROR, NULL);

        EcsMetaTypeSerialized *ptr = ecs_get_mut(
            world, e, EcsMetaTypeSerialized);
        if (ptr->ops) {
            ecs_meta_dtor_serialized(ptr);
        }

        ptr->ops = ops;
    }
}

#endif


#ifdef FLECS_META

/* EcsMetaTypeSerialized lifecycle */

void ecs_meta_dtor_serialized(
    EcsMetaTypeSerialized *ptr) 
{
    int32_t i, count = ecs_vector_count(ptr->ops);
    ecs_meta_type_op_t *ops = ecs_vector_first(ptr->ops, ecs_meta_type_op_t);
    
    for (i = 0; i < count; i ++) {
        ecs_meta_type_op_t *op = &ops[i];
        if (op->members) {
            flecs_hashmap_fini(op->members);
            ecs_os_free(op->members);
        }
    }

    ecs_vector_free(ptr->ops); 
}

static ECS_COPY(EcsMetaTypeSerialized, dst, src, {
    ecs_meta_dtor_serialized(dst);

    dst->ops = ecs_vector_copy(src->ops, ecs_meta_type_op_t);

    int32_t o, count = ecs_vector_count(src->ops);
    ecs_meta_type_op_t *ops = ecs_vector_first(src->ops, ecs_meta_type_op_t);
    
    for (o = 0; o < count; o ++) {
        ecs_meta_type_op_t *op = &ops[o];
        if (op->members) {
            op->members = ecs_os_memdup_t(op->members, ecs_hashmap_t);
            flecs_hashmap_copy(op->members, op->members);
        }
    }
})

static ECS_MOVE(EcsMetaTypeSerialized, dst, src, {
    ecs_meta_dtor_serialized(dst);
    dst->ops = src->ops;
    src->ops = NULL;
})

static ECS_DTOR(EcsMetaTypeSerialized, ptr, { 
    ecs_meta_dtor_serialized(ptr);
})


/* EcsStruct lifecycle */

static void dtor_struct(
    EcsStruct *ptr) 
{
    ecs_member_t *members = ecs_vector_first(ptr->members, ecs_member_t);
    int32_t i, count = ecs_vector_count(ptr->members);
    for (i = 0; i < count; i ++) {
        ecs_os_free((char*)members[i].name);
    }
    ecs_vector_free(ptr->members);
}

static ECS_COPY(EcsStruct, dst, src, {
    dtor_struct(dst);

    dst->members = ecs_vector_copy(src->members, ecs_member_t);

    ecs_member_t *members = ecs_vector_first(dst->members, ecs_member_t);
    int32_t m, count = ecs_vector_count(dst->members);

    for (m = 0; m < count; m ++) {
        members[m].name = ecs_os_strdup(members[m].name);
    }
})

static ECS_MOVE(EcsStruct, dst, src, {
    dtor_struct(dst);
    dst->members = src->members;
    src->members = NULL;
})

static ECS_DTOR(EcsStruct, ptr, { dtor_struct(ptr); })


/* EcsEnum lifecycle */

static void dtor_enum(
    EcsEnum *ptr) 
{
    ecs_map_iter_t it = ecs_map_iter(ptr->constants);
    ecs_enum_constant_t *c;
    while ((c = ecs_map_next(&it, ecs_enum_constant_t, NULL))) {
        ecs_os_free((char*)c->name);
    }
    ecs_map_free(ptr->constants);
}

static ECS_COPY(EcsEnum, dst, src, {
    dtor_enum(dst);

    dst->constants = ecs_map_copy(src->constants);
    ecs_assert(ecs_map_count(dst->constants) == ecs_map_count(src->constants),
        ECS_INTERNAL_ERROR, NULL);

    ecs_map_iter_t it = ecs_map_iter(dst->constants);
    ecs_enum_constant_t *c;
    while ((c = ecs_map_next(&it, ecs_enum_constant_t, NULL))) {
        c->name = ecs_os_strdup(c->name);
    }
})

static ECS_MOVE(EcsEnum, dst, src, {
    dtor_enum(dst);
    dst->constants = src->constants;
    src->constants = NULL;
})

static ECS_DTOR(EcsEnum, ptr, { dtor_enum(ptr); })


/* EcsBitmask lifecycle */

static void dtor_bitmask(
    EcsBitmask *ptr) 
{
    ecs_map_iter_t it = ecs_map_iter(ptr->constants);
    ecs_bitmask_constant_t *c;
    while ((c = ecs_map_next(&it, ecs_bitmask_constant_t, NULL))) {
        ecs_os_free((char*)c->name);
    }
    ecs_map_free(ptr->constants);
}

static ECS_COPY(EcsBitmask, dst, src, {
    dtor_bitmask(dst);

    dst->constants = ecs_map_copy(src->constants);
    ecs_assert(ecs_map_count(dst->constants) == ecs_map_count(src->constants),
        ECS_INTERNAL_ERROR, NULL);

    ecs_map_iter_t it = ecs_map_iter(dst->constants);
    ecs_bitmask_constant_t *c;
    while ((c = ecs_map_next(&it, ecs_bitmask_constant_t, NULL))) {
        c->name = ecs_os_strdup(c->name);
    }
})

static ECS_MOVE(EcsBitmask, dst, src, {
    dtor_bitmask(dst);
    dst->constants = src->constants;
    src->constants = NULL;
})

static ECS_DTOR(EcsBitmask, ptr, { dtor_bitmask(ptr); })


/* EcsUnit lifecycle */

static void dtor_unit(
    EcsUnit *ptr) 
{
    ecs_os_free(ptr->symbol);
}

static ECS_COPY(EcsUnit, dst, src, {
    dtor_unit(dst);
    dst->symbol = ecs_os_strdup(src->symbol);
    dst->base = src->base;
    dst->over = src->over;
    dst->prefix = src->prefix;
    dst->translation = src->translation;
})

static ECS_MOVE(EcsUnit, dst, src, {
    dtor_unit(dst);
    dst->symbol = src->symbol;
    dst->base = src->base;
    dst->over = src->over;
    dst->prefix = src->prefix;
    dst->translation = src->translation;

    src->symbol = NULL;
    src->base = 0;
    src->over = 0;
    src->prefix = 0;
    src->translation = (ecs_unit_translation_t){0};
})

static ECS_DTOR(EcsUnit, ptr, { dtor_unit(ptr); })


/* EcsUnitPrefix lifecycle */

static void dtor_unit_prefix(
    EcsUnitPrefix *ptr) 
{
    ecs_os_free(ptr->symbol);
}

static ECS_COPY(EcsUnitPrefix, dst, src, {
    dtor_unit_prefix(dst);
    dst->symbol = ecs_os_strdup(src->symbol);
    dst->translation = src->translation;
})

static ECS_MOVE(EcsUnitPrefix, dst, src, {
    dtor_unit_prefix(dst);
    dst->symbol = src->symbol;
    dst->translation = src->translation;

    src->symbol = NULL;
    src->translation = (ecs_unit_translation_t){0};
})

static ECS_DTOR(EcsUnitPrefix, ptr, { dtor_unit_prefix(ptr); })


/* Type initialization */

static
int init_type(
    ecs_world_t *world,
    ecs_entity_t type,
    ecs_type_kind_t kind,
    ecs_size_t size,
    ecs_size_t alignment)
{
    ecs_assert(world != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(type != 0, ECS_INTERNAL_ERROR, NULL);

    EcsMetaType *meta_type = ecs_get_mut(world, type, EcsMetaType);
    if (meta_type->kind == 0) {
        meta_type->existing = ecs_has(world, type, EcsComponent);

        /* Ensure that component has a default constructor, to prevent crashing
         * serializers on uninitialized values. */
        ecs_type_info_t *ti = flecs_type_info_ensure(world, type);
        if (!ti->hooks.ctor) {
            ti->hooks.ctor = ecs_default_ctor;
        }
    } else {
        if (meta_type->kind != kind) {
            ecs_err("type '%s' reregistered with different kind", 
                ecs_get_name(world, type));
            return -1;
        }
    }

    if (!meta_type->existing) {
        EcsComponent *comp = ecs_get_mut(world, type, EcsComponent);
        comp->size = size;
        comp->alignment = alignment;
        ecs_modified(world, type, EcsComponent);
    } else {
        const EcsComponent *comp = ecs_get(world, type, EcsComponent);
        if (comp->size < size) {
            ecs_err("computed size for '%s' is larger than actual type", 
                ecs_get_name(world, type));
            return -1;
        }
        if (comp->alignment < alignment) {
            ecs_err("computed alignment for '%s' is larger than actual type", 
                ecs_get_name(world, type));
            return -1;
        }
        if (comp->size == size && comp->alignment != alignment) {
            ecs_err("computed size for '%s' matches with actual type but "
                "alignment is different", ecs_get_name(world, type));
            return -1;
        }
        
        meta_type->partial = comp->size != size;
    }

    meta_type->kind = kind;
    meta_type->size = size;
    meta_type->alignment = alignment;
    ecs_modified(world, type, EcsMetaType);

    return 0;
}

#define init_type_t(world, type, kind, T) \
    init_type(world, type, kind, ECS_SIZEOF(T), ECS_ALIGNOF(T))

static
void set_struct_member(
    ecs_member_t *member,
    ecs_entity_t entity,
    const char *name,
    ecs_entity_t type,
    int32_t count,
    int32_t offset,
    ecs_entity_t unit)
{
    member->member = entity;
    member->type = type;
    member->count = count;
    member->unit = unit;
    member->offset = offset;

    if (!count) {
        member->count = 1;
    }

    ecs_os_strset((char**)&member->name, name);
}

static
int add_member_to_struct(
    ecs_world_t *world,
    ecs_entity_t type,
    ecs_entity_t member,
    EcsMember *m)
{
    ecs_assert(world != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(type != 0, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(member != 0, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(m != NULL, ECS_INTERNAL_ERROR, NULL);

    const char *name = ecs_get_name(world, member);
    if (!name) {
        char *path = ecs_get_fullpath(world, type);
        ecs_err("member for struct '%s' does not have a name", path);
        ecs_os_free(path);
        return -1;
    }

    if (!m->type) {
        char *path = ecs_get_fullpath(world, member);
        ecs_err("member '%s' does not have a type", path);
        ecs_os_free(path);
        return -1;
    }

    if (ecs_get_typeid(world, m->type) == 0) {
        char *path = ecs_get_fullpath(world, member);
        char *ent_path = ecs_get_fullpath(world, m->type);
        ecs_err("member '%s.type' is '%s' which is not a type", path, ent_path);
        ecs_os_free(path);
        ecs_os_free(ent_path);
        return -1;
    }

    ecs_entity_t unit = m->unit;

    if (unit) {
        if (!ecs_has(world, unit, EcsUnit)) {
            ecs_err("entity '%s' for member '%s' is not a unit",
                ecs_get_name(world, unit), name);
            return -1;
        }

        if (ecs_has(world, m->type, EcsUnit) && m->type != unit) {
            ecs_err("unit mismatch for type '%s' and unit '%s' for member '%s'",
                ecs_get_name(world, m->type), ecs_get_name(world, unit), name);
            return -1;
        }
    } else {
        if (ecs_has(world, m->type, EcsUnit)) {
            unit = m->type;
            m->unit = unit;
        }
    }

    EcsStruct *s = ecs_get_mut(world, type, EcsStruct);
    ecs_assert(s != NULL, ECS_INTERNAL_ERROR, NULL);

    /* First check if member is already added to struct */
    ecs_member_t *members = ecs_vector_first(s->members, ecs_member_t);
    int32_t i, count = ecs_vector_count(s->members);
    for (i = 0; i < count; i ++) {
        if (members[i].member == member) {
            set_struct_member(
                &members[i], member, name, m->type, m->count, m->offset, unit);
            break;
        }
    }

    /* If member wasn't added yet, add a new element to vector */
    if (i == count) {
        ecs_member_t *elem = ecs_vector_add(&s->members, ecs_member_t);
        elem->name = NULL;
        set_struct_member(elem, member, name, m->type, 
            m->count, m->offset, unit);

        /* Reobtain members array in case it was reallocated */
        members = ecs_vector_first(s->members, ecs_member_t);
        count ++;
    }

    bool explicit_offset = false;
    if (m->offset) {
        explicit_offset = true;
    }

    /* Compute member offsets and size & alignment of struct */
    ecs_size_t size = 0;
    ecs_size_t alignment = 0;

    if (!explicit_offset) {
        for (i = 0; i < count; i ++) {
            ecs_member_t *elem = &members[i];

            ecs_assert(elem->name != NULL, ECS_INTERNAL_ERROR, NULL);
            ecs_assert(elem->type != 0, ECS_INTERNAL_ERROR, NULL);

            /* Get component of member type to get its size & alignment */
            const EcsComponent *mbr_comp = ecs_get(world, elem->type, EcsComponent);
            if (!mbr_comp) {
                char *path = ecs_get_fullpath(world, member);
                ecs_err("member '%s' is not a type", path);
                ecs_os_free(path);
                return -1;
            }

            ecs_size_t member_size = mbr_comp->size;
            ecs_size_t member_alignment = mbr_comp->alignment;

            if (!member_size || !member_alignment) {
                char *path = ecs_get_fullpath(world, member);
                ecs_err("member '%s' has 0 size/alignment");
                ecs_os_free(path);
                return -1;
            }

            member_size *= elem->count;
            size = ECS_ALIGN(size, member_alignment);
            elem->size = member_size;
            elem->offset = size;

            size += member_size;

            if (member_alignment > alignment) {
                alignment = member_alignment;
            }
        }
    } else {
        /* If members have explicit offsets, we can't rely on computed 
         * size/alignment values. Grab size of just added member instead. It
         * doesn't matter if the size doesn't correspond to the actual struct
         * size. The init_type function compares computed size with actual
         * (component) size to determine if the type is partial. */
        const EcsComponent *cptr = ecs_get(world, m->type, EcsComponent);
        ecs_assert(cptr != NULL, ECS_INTERNAL_ERROR, NULL);
        size = cptr->size;
        alignment = cptr->alignment;
    }

    if (size == 0) {
        ecs_err("struct '%s' has 0 size", ecs_get_name(world, type));
        return -1;
    }

    if (alignment == 0) {
        ecs_err("struct '%s' has 0 alignment", ecs_get_name(world, type));
        return -1;
    }

    /* Align struct size to struct alignment */
    size = ECS_ALIGN(size, alignment);

    ecs_modified(world, type, EcsStruct);

    /* Do this last as it triggers the update of EcsMetaTypeSerialized */
    if (init_type(world, type, EcsStructType, size, alignment)) {
        return -1;
    }

    /* If current struct is also a member, assign to itself */
    if (ecs_has(world, type, EcsMember)) {
        EcsMember *type_mbr = ecs_get_mut(world, type, EcsMember);
        ecs_assert(type_mbr != NULL, ECS_INTERNAL_ERROR, NULL);

        type_mbr->type = type;
        type_mbr->count = 1;

        ecs_modified(world, type, EcsMember);
    }

    return 0;
}

static
int add_constant_to_enum(
    ecs_world_t *world, 
    ecs_entity_t type, 
    ecs_entity_t e,
    ecs_id_t constant_id)
{
    EcsEnum *ptr = ecs_get_mut(world, type, EcsEnum);
    
    /* Remove constant from map if it was already added */
    ecs_map_iter_t it = ecs_map_iter(ptr->constants);
    ecs_enum_constant_t *c;
    ecs_map_key_t key;

    while ((c = ecs_map_next(&it, ecs_enum_constant_t, &key))) {
        if (c->constant == e) {
            ecs_os_free((char*)c->name);
            ecs_map_remove(ptr->constants, key);
        }
    }

    /* Check if constant sets explicit value */
    int32_t value = 0;
    bool value_set = false;
    if (ecs_id_is_pair(constant_id)) {
        if (ecs_pair_second(world, constant_id) != ecs_id(ecs_i32_t)) {
            char *path = ecs_get_fullpath(world, e);
            ecs_err("expected i32 type for enum constant '%s'", path);
            ecs_os_free(path);
            return -1;
        }

        const int32_t *value_ptr = ecs_get_pair_object(
            world, e, EcsConstant, ecs_i32_t);
        ecs_assert(value_ptr != NULL, ECS_INTERNAL_ERROR, NULL);
        value = *value_ptr;
        value_set = true;
    }

    /* Make sure constant value doesn't conflict if set / find the next value */
    it = ecs_map_iter(ptr->constants);
    while  ((c = ecs_map_next(&it, ecs_enum_constant_t, &key))) {
        if (value_set) {
            if (c->value == value) {
                char *path = ecs_get_fullpath(world, e);
                ecs_err("conflicting constant value for '%s' (other is '%s')",
                    path, c->name);
                ecs_os_free(path);
                return -1;
            }
        } else {
            if (c->value >= value) {
                value = c->value + 1;
            }
        }
    }

    if (!ptr->constants) {
        ptr->constants = ecs_map_new(ecs_enum_constant_t, 1);
    }

    c = ecs_map_ensure(ptr->constants, ecs_enum_constant_t, value);
    c->name = ecs_os_strdup(ecs_get_name(world, e));
    c->value = value;
    c->constant = e;

    ecs_i32_t *cptr = ecs_get_mut_pair_object(
        world, e, EcsConstant, ecs_i32_t);
    ecs_assert(cptr != NULL, ECS_INTERNAL_ERROR, NULL);
    cptr[0] = value;

    return 0;
}

static
int add_constant_to_bitmask(
    ecs_world_t *world, 
    ecs_entity_t type, 
    ecs_entity_t e,
    ecs_id_t constant_id)
{
    EcsBitmask *ptr = ecs_get_mut(world, type, EcsBitmask);
    
    /* Remove constant from map if it was already added */
    ecs_map_iter_t it = ecs_map_iter(ptr->constants);
    ecs_bitmask_constant_t *c;
    ecs_map_key_t key;
    while ((c = ecs_map_next(&it, ecs_bitmask_constant_t, &key))) {
        if (c->constant == e) {
            ecs_os_free((char*)c->name);
            ecs_map_remove(ptr->constants, key);
        }
    }

    /* Check if constant sets explicit value */
    uint32_t value = 1;
    if (ecs_id_is_pair(constant_id)) {
        if (ecs_pair_second(world, constant_id) != ecs_id(ecs_u32_t)) {
            char *path = ecs_get_fullpath(world, e);
            ecs_err("expected u32 type for bitmask constant '%s'", path);
            ecs_os_free(path);
            return -1;
        }

        const uint32_t *value_ptr = ecs_get_pair_object(
            world, e, EcsConstant, ecs_u32_t);
        ecs_assert(value_ptr != NULL, ECS_INTERNAL_ERROR, NULL);
        value = *value_ptr;
    } else {
        value = 1u << (ecs_u32_t)ecs_map_count(ptr->constants);
    }

    /* Make sure constant value doesn't conflict */
    it = ecs_map_iter(ptr->constants);
    while  ((c = ecs_map_next(&it, ecs_bitmask_constant_t, &key))) {
        if (c->value == value) {
            char *path = ecs_get_fullpath(world, e);
            ecs_err("conflicting constant value for '%s' (other is '%s')",
                path, c->name);
            ecs_os_free(path);
            return -1;
        }
    }

    if (!ptr->constants) {
        ptr->constants = ecs_map_new(ecs_bitmask_constant_t, 1);
    }

    c = ecs_map_ensure(ptr->constants, ecs_bitmask_constant_t, value);
    c->name = ecs_os_strdup(ecs_get_name(world, e));
    c->value = value;
    c->constant = e;

    ecs_u32_t *cptr = ecs_get_mut_pair_object(
        world, e, EcsConstant, ecs_u32_t);
    ecs_assert(cptr != NULL, ECS_INTERNAL_ERROR, NULL);
    cptr[0] = value;

    return 0;
}

static
void set_primitive(ecs_iter_t *it) {
    ecs_world_t *world = it->world;
    EcsPrimitive *type = ecs_field(it, EcsPrimitive, 1);

    int i, count = it->count;
    for (i = 0; i < count; i ++) {
        ecs_entity_t e = it->entities[i];
        switch(type->kind) {
        case EcsBool:
            init_type_t(world, e, EcsPrimitiveType, bool);
            break;
        case EcsChar:
            init_type_t(world, e, EcsPrimitiveType, char);
            break;
        case EcsByte:
            init_type_t(world, e, EcsPrimitiveType, bool);
            break;
        case EcsU8:
            init_type_t(world, e, EcsPrimitiveType, uint8_t);
            break;
        case EcsU16:
            init_type_t(world, e, EcsPrimitiveType, uint16_t);
            break;
        case EcsU32:
            init_type_t(world, e, EcsPrimitiveType, uint32_t);
            break;
        case EcsU64:
            init_type_t(world, e, EcsPrimitiveType, uint64_t);
            break;
        case EcsI8:
            init_type_t(world, e, EcsPrimitiveType, int8_t);
            break;
        case EcsI16:
            init_type_t(world, e, EcsPrimitiveType, int16_t);
            break;
        case EcsI32:
            init_type_t(world, e, EcsPrimitiveType, int32_t);
            break;
        case EcsI64:
            init_type_t(world, e, EcsPrimitiveType, int64_t);
            break;
        case EcsF32:
            init_type_t(world, e, EcsPrimitiveType, float);
            break;
        case EcsF64:
            init_type_t(world, e, EcsPrimitiveType, double);
            break;
        case EcsUPtr:
            init_type_t(world, e, EcsPrimitiveType, uintptr_t);
            break;
        case EcsIPtr:
            init_type_t(world, e, EcsPrimitiveType, intptr_t);
            break;
        case EcsString:
            init_type_t(world, e, EcsPrimitiveType, char*);
            break;
        case EcsEntity:
            init_type_t(world, e, EcsPrimitiveType, ecs_entity_t);
            break;
        }
    }
}

static
void set_member(ecs_iter_t *it) {
    ecs_world_t *world = it->world;
    EcsMember *member = ecs_field(it, EcsMember, 1);

    int i, count = it->count;
    for (i = 0; i < count; i ++) {
        ecs_entity_t e = it->entities[i];
        ecs_entity_t parent = ecs_get_target(world, e, EcsChildOf, 0);
        if (!parent) {
            ecs_err("missing parent for member '%s'", ecs_get_name(world, e));
            continue;
        }

        add_member_to_struct(world, parent, e, &member[i]);
    }
}

static
void add_enum(ecs_iter_t *it) {
    ecs_world_t *world = it->world;

    int i, count = it->count;
    for (i = 0; i < count; i ++) {
        ecs_entity_t e = it->entities[i];

        if (init_type_t(world, e, EcsEnumType, ecs_i32_t)) {
            continue;
        }

        ecs_add_id(world, e, EcsExclusive);
        ecs_add_id(world, e, EcsOneOf);
        ecs_add_id(world, e, EcsTag);
    }
}

static
void add_bitmask(ecs_iter_t *it) {
    ecs_world_t *world = it->world;

    int i, count = it->count;
    for (i = 0; i < count; i ++) {
        ecs_entity_t e = it->entities[i];

        if (init_type_t(world, e, EcsBitmaskType, ecs_u32_t)) {
            continue;
        }
    }
}

static
void add_constant(ecs_iter_t *it) {
    ecs_world_t *world = it->world;

    int i, count = it->count;
    for (i = 0; i < count; i ++) {
        ecs_entity_t e = it->entities[i];
        ecs_entity_t parent = ecs_get_target(world, e, EcsChildOf, 0);
        if (!parent) {
            ecs_err("missing parent for constant '%s'", ecs_get_name(world, e));
            continue;
        }

        if (ecs_has(world, parent, EcsEnum)) {
            add_constant_to_enum(world, parent, e, it->event_id);
        } else if (ecs_has(world, parent, EcsBitmask)) {
            add_constant_to_bitmask(world, parent, e, it->event_id);
        }
    }
}

static
void set_array(ecs_iter_t *it) {
    ecs_world_t *world = it->world;
    EcsArray *array = ecs_field(it, EcsArray, 1);

    int i, count = it->count;
    for (i = 0; i < count; i ++) {
        ecs_entity_t e = it->entities[i];
        ecs_entity_t elem_type = array[i].type;
        int32_t elem_count = array[i].count;

        if (!elem_type) {
            ecs_err("array '%s' has no element type", ecs_get_name(world, e));
            continue;
        }

        if (!elem_count) {
            ecs_err("array '%s' has size 0", ecs_get_name(world, e));
            continue;
        }

        const EcsComponent *elem_ptr = ecs_get(world, elem_type, EcsComponent);
        if (init_type(world, e, EcsArrayType, 
            elem_ptr->size * elem_count, elem_ptr->alignment)) 
        {
            continue;
        }
    }
}

static
void set_vector(ecs_iter_t *it) {
    ecs_world_t *world = it->world;
    EcsVector *array = ecs_field(it, EcsVector, 1);

    int i, count = it->count;
    for (i = 0; i < count; i ++) {
        ecs_entity_t e = it->entities[i];
        ecs_entity_t elem_type = array[i].type;

        if (!elem_type) {
            ecs_err("vector '%s' has no element type", ecs_get_name(world, e));
            continue;
        }

        if (init_type_t(world, e, EcsVectorType, ecs_vector_t*)) {
            continue;
        }
    }
}

bool flecs_unit_validate(
    ecs_world_t *world,
    ecs_entity_t t,
    EcsUnit *data)
{
    char *derived_symbol = NULL;
    const char *symbol = data->symbol;

    ecs_entity_t base = data->base;
    ecs_entity_t over = data->over;
    ecs_entity_t prefix = data->prefix;
    ecs_unit_translation_t translation = data->translation;

    if (base) {
        if (!ecs_has(world, base, EcsUnit)) {
            ecs_err("entity '%s' for unit '%s' used as base is not a unit",
                ecs_get_name(world, base), ecs_get_name(world, t));
            goto error;
        }
    }

    if (over) {
        if (!base) {
            ecs_err("invalid unit '%s': cannot specify over without base",
                ecs_get_name(world, t));
            goto error;
        }
        if (!ecs_has(world, over, EcsUnit)) {
            ecs_err("entity '%s' for unit '%s' used as over is not a unit",
                ecs_get_name(world, over), ecs_get_name(world, t));
            goto error;
        }
    }

    if (prefix) {
        if (!base) {
            ecs_err("invalid unit '%s': cannot specify prefix without base",
                ecs_get_name(world, t));
            goto error;
        }
        const EcsUnitPrefix *prefix_ptr = ecs_get(world, prefix, EcsUnitPrefix);
        if (!prefix_ptr) {
            ecs_err("entity '%s' for unit '%s' used as prefix is not a prefix",
                ecs_get_name(world, over), ecs_get_name(world, t));
            goto error;
        }

        if (translation.factor || translation.power) {
            if (prefix_ptr->translation.factor != translation.factor ||
                prefix_ptr->translation.power != translation.power)
            {
                ecs_err(
                    "factor for unit '%s' is inconsistent with prefix '%s'",
                    ecs_get_name(world, t), ecs_get_name(world, prefix));
                goto error;
            }
        } else {
            translation = prefix_ptr->translation;
        }
    }

    if (base) {
        bool must_match = false; /* Must base symbol match symbol? */
        ecs_strbuf_t sbuf = ECS_STRBUF_INIT;
        if (prefix) {
            const EcsUnitPrefix *ptr = ecs_get(world, prefix, EcsUnitPrefix);
            ecs_assert(ptr != NULL, ECS_INTERNAL_ERROR, NULL);
            if (ptr->symbol) {
                ecs_strbuf_appendstr(&sbuf, ptr->symbol);
                must_match = true;
            }
        }

        const EcsUnit *uptr = ecs_get(world, base, EcsUnit);
        ecs_assert(uptr != NULL, ECS_INTERNAL_ERROR, NULL);
        if (uptr->symbol) {
            ecs_strbuf_appendstr(&sbuf, uptr->symbol);
        }

        if (over) {
            uptr = ecs_get(world, over, EcsUnit);
            ecs_assert(uptr != NULL, ECS_INTERNAL_ERROR, NULL);
            if (uptr->symbol) {
                ecs_strbuf_appendstr(&sbuf, "/");
                ecs_strbuf_appendstr(&sbuf, uptr->symbol);
                must_match = true;
            }
        }

        derived_symbol = ecs_strbuf_get(&sbuf);
        if (derived_symbol && !ecs_os_strlen(derived_symbol)) {
            ecs_os_free(derived_symbol);
            derived_symbol = NULL;
        }

        if (derived_symbol && symbol && ecs_os_strcmp(symbol, derived_symbol)) {
            if (must_match) {
                ecs_err("symbol '%s' for unit '%s' does not match base"
                    " symbol '%s'", symbol, 
                        ecs_get_name(world, t), derived_symbol);
                goto error;
            }
        }
        if (!symbol && derived_symbol && (prefix || over)) {
            ecs_os_free(data->symbol);
            data->symbol = derived_symbol;
        } else {
            ecs_os_free(derived_symbol);
        }
    }

    data->base = base;
    data->over = over;
    data->prefix = prefix;
    data->translation = translation;

    return true;
error:
    ecs_os_free(derived_symbol);
    return false;
}

static
void set_unit(ecs_iter_t *it) {
    EcsUnit *u = ecs_field(it, EcsUnit, 1);

    ecs_world_t *world = it->world;

    int i, count = it->count;
    for (i = 0; i < count; i ++) {
        ecs_entity_t e = it->entities[i];
        flecs_unit_validate(world, e, &u[i]);
    }
}

static
void unit_quantity_monitor(ecs_iter_t *it) {
    ecs_world_t *world = it->world;

    int i, count = it->count;
    if (it->event == EcsOnAdd) {
        for (i = 0; i < count; i ++) {
            ecs_entity_t e = it->entities[i];
            ecs_add_pair(world, e, EcsQuantity, e);
        }
    } else {
        for (i = 0; i < count; i ++) {
            ecs_entity_t e = it->entities[i];
            ecs_remove_pair(world, e, EcsQuantity, e);
        }
    }
}

static
void ecs_meta_type_init_default_ctor(ecs_iter_t *it) {
    ecs_world_t *world = it->world;
    EcsMetaType *type = ecs_field(it, EcsMetaType, 1);

    int i;
    for (i = 0; i < it->count; i ++) {
        /* If a component is defined from reflection data, configure it with the
         * default constructor. This ensures that a new component value does not
         * contain uninitialized memory, which could cause serializers to crash
         * when for example inspecting string fields. */
        if (!type->existing) {
            ecs_set_hooks_id(world, it->entities[i], 
                &(ecs_type_hooks_t){ 
                    .ctor = ecs_default_ctor
                });
        }
    }
}

static
void member_on_set(ecs_iter_t *it) {
    EcsMember *mbr = it->ptrs[0];
    if (!mbr->count) {
        mbr->count = 1;
    }
}

void FlecsMetaImport(
    ecs_world_t *world)
{
    ECS_MODULE(world, FlecsMeta);

    ecs_set_name_prefix(world, "Ecs");

    flecs_bootstrap_component(world, EcsMetaType);
    flecs_bootstrap_component(world, EcsMetaTypeSerialized);
    flecs_bootstrap_component(world, EcsPrimitive);
    flecs_bootstrap_component(world, EcsEnum);
    flecs_bootstrap_component(world, EcsBitmask);
    flecs_bootstrap_component(world, EcsMember);
    flecs_bootstrap_component(world, EcsStruct);
    flecs_bootstrap_component(world, EcsArray);
    flecs_bootstrap_component(world, EcsVector);
    flecs_bootstrap_component(world, EcsUnit);
    flecs_bootstrap_component(world, EcsUnitPrefix);

    flecs_bootstrap_tag(world, EcsConstant);
    flecs_bootstrap_tag(world, EcsQuantity);

    ecs_set_hooks(world, EcsMetaType, { .ctor = ecs_default_ctor });

    ecs_set_hooks(world, EcsMetaTypeSerialized, { 
        .ctor = ecs_default_ctor,
        .move = ecs_move(EcsMetaTypeSerialized),
        .copy = ecs_copy(EcsMetaTypeSerialized),
        .dtor = ecs_dtor(EcsMetaTypeSerialized)
    });

    ecs_set_hooks(world, EcsStruct, { 
        .ctor = ecs_default_ctor,
        .move = ecs_move(EcsStruct),
        .copy = ecs_copy(EcsStruct),
        .dtor = ecs_dtor(EcsStruct)
    });

    ecs_set_hooks(world, EcsMember, { 
        .ctor = ecs_default_ctor,
        .on_set = member_on_set
    });

    ecs_set_hooks(world, EcsEnum, { 
        .ctor = ecs_default_ctor,
        .move = ecs_move(EcsEnum),
        .copy = ecs_copy(EcsEnum),
        .dtor = ecs_dtor(EcsEnum)
    });

    ecs_set_hooks(world, EcsBitmask, { 
        .ctor = ecs_default_ctor,
        .move = ecs_move(EcsBitmask),
        .copy = ecs_copy(EcsBitmask),
        .dtor = ecs_dtor(EcsBitmask)
    });

    ecs_set_hooks(world, EcsUnit, { 
        .ctor = ecs_default_ctor,
        .move = ecs_move(EcsUnit),
        .copy = ecs_copy(EcsUnit),
        .dtor = ecs_dtor(EcsUnit)
    });

    ecs_set_hooks(world, EcsUnitPrefix, { 
        .ctor = ecs_default_ctor,
        .move = ecs_move(EcsUnitPrefix),
        .copy = ecs_copy(EcsUnitPrefix),
        .dtor = ecs_dtor(EcsUnitPrefix)
    });

    /* Register triggers to finalize type information from component data */
    ecs_entity_t old_scope = ecs_set_scope( /* Keep meta scope clean */
        world, EcsFlecsInternals);
    ecs_observer_init(world, &(ecs_observer_desc_t){
        .filter.terms[0] = { .id = ecs_id(EcsPrimitive), .src.flags = EcsSelf },
        .events = {EcsOnSet},
        .callback = set_primitive
    });

    ecs_observer_init(world, &(ecs_observer_desc_t){
        .filter.terms[0] = { .id = ecs_id(EcsMember), .src.flags = EcsSelf },
        .events = {EcsOnSet},
        .callback = set_member
    });

    ecs_observer_init(world, &(ecs_observer_desc_t){
        .filter.terms[0] = { .id = ecs_id(EcsEnum), .src.flags = EcsSelf },
        .events = {EcsOnAdd},
        .callback = add_enum
    });

    ecs_observer_init(world, &(ecs_observer_desc_t){
        .filter.terms[0] = { .id = ecs_id(EcsBitmask), .src.flags = EcsSelf },
        .events = {EcsOnAdd},
        .callback = add_bitmask
    });

    ecs_observer_init(world, &(ecs_observer_desc_t){
        .filter.terms[0] = { .id = EcsConstant, .src.flags = EcsSelf },
        .events = {EcsOnAdd},
        .callback = add_constant
    });

    ecs_observer_init(world, &(ecs_observer_desc_t){
        .filter.terms[0] = { .id = ecs_pair(EcsConstant, EcsWildcard), .src.flags = EcsSelf },
        .events = {EcsOnSet},
        .callback = add_constant
    });

    ecs_observer_init(world, &(ecs_observer_desc_t){
        .filter.terms[0] = { .id = ecs_id(EcsArray), .src.flags = EcsSelf },
        .events = {EcsOnSet},
        .callback = set_array
    });

    ecs_observer_init(world, &(ecs_observer_desc_t){
        .filter.terms[0] = { .id = ecs_id(EcsVector), .src.flags = EcsSelf },
        .events = {EcsOnSet},
        .callback = set_vector
    });

    ecs_observer_init(world, &(ecs_observer_desc_t){
        .filter.terms[0] = { .id = ecs_id(EcsUnit), .src.flags = EcsSelf },
        .events = {EcsOnSet},
        .callback = set_unit
    });

    ecs_observer_init(world, &(ecs_observer_desc_t){
        .filter.terms[0] = { .id = ecs_id(EcsMetaType), .src.flags = EcsSelf },
        .events = {EcsOnSet},
        .callback = ecs_meta_type_serialized_init
    });

    ecs_observer_init(world, &(ecs_observer_desc_t){
        .filter.terms[0] = { .id = ecs_id(EcsMetaType), .src.flags = EcsSelf },
        .events = {EcsOnSet},
        .callback = ecs_meta_type_init_default_ctor
    });

    ecs_observer_init(world, &(ecs_observer_desc_t){
        .filter.terms = {
            { .id = ecs_id(EcsUnit) },
            { .id = EcsQuantity }
        },
        .events = { EcsMonitor },
        .callback = unit_quantity_monitor
    });
    ecs_set_scope(world, old_scope);

    /* Initialize primitive types */
    #define ECS_PRIMITIVE(world, type, primitive_kind)\
        ecs_entity_init(world, &(ecs_entity_desc_t){\
            .id = ecs_id(ecs_##type##_t),\
            .name = #type,\
            .symbol = #type });\
        ecs_set(world, ecs_id(ecs_##type##_t), EcsPrimitive, {\
            .kind = primitive_kind\
        });

    ECS_PRIMITIVE(world, bool, EcsBool);
    ECS_PRIMITIVE(world, char, EcsChar);
    ECS_PRIMITIVE(world, byte, EcsByte);
    ECS_PRIMITIVE(world, u8, EcsU8);
    ECS_PRIMITIVE(world, u16, EcsU16);
    ECS_PRIMITIVE(world, u32, EcsU32);
    ECS_PRIMITIVE(world, u64, EcsU64);
    ECS_PRIMITIVE(world, uptr, EcsUPtr);
    ECS_PRIMITIVE(world, i8, EcsI8);
    ECS_PRIMITIVE(world, i16, EcsI16);
    ECS_PRIMITIVE(world, i32, EcsI32);
    ECS_PRIMITIVE(world, i64, EcsI64);
    ECS_PRIMITIVE(world, iptr, EcsIPtr);
    ECS_PRIMITIVE(world, f32, EcsF32);
    ECS_PRIMITIVE(world, f64, EcsF64);
    ECS_PRIMITIVE(world, string, EcsString);
    ECS_PRIMITIVE(world, entity, EcsEntity);

    #undef ECS_PRIMITIVE

    /* Set default child components */
    ecs_add_pair(world, ecs_id(EcsStruct), 
        EcsDefaultChildComponent, ecs_id(EcsMember));

    ecs_add_pair(world, ecs_id(EcsMember), 
        EcsDefaultChildComponent, ecs_id(EcsMember));

    ecs_add_pair(world, ecs_id(EcsEnum), 
        EcsDefaultChildComponent, EcsConstant);

    ecs_add_pair(world, ecs_id(EcsBitmask), 
        EcsDefaultChildComponent, EcsConstant);

    /* Relationship properties */
    ecs_add_id(world, EcsQuantity, EcsExclusive);
    ecs_add_id(world, EcsQuantity, EcsTag);

    /* Initialize reflection data for meta components */
    ecs_entity_t type_kind = ecs_enum_init(world, &(ecs_enum_desc_t){
        .entity = ecs_entity(world, { .name = "TypeKind" }),
        .constants = {
            {.name = "PrimitiveType"},
            {.name = "BitmaskType"},
            {.name = "EnumType"},
            {.name = "StructType"},
            {.name = "ArrayType"},
            {.name = "VectorType"}
        }
    });

    ecs_struct_init(world, &(ecs_struct_desc_t){
        .entity = ecs_id(EcsMetaType),
        .members = {
            {.name = (char*)"kind", .type = type_kind}
        }
    });

    ecs_entity_t primitive_kind = ecs_enum_init(world, &(ecs_enum_desc_t){
        .entity = ecs_entity(world, { .name = "PrimitiveKind" }),
        .constants = {
            {.name = "Bool", 1}, 
            {.name = "Char"}, 
            {.name = "Byte"}, 
            {.name = "U8"}, 
            {.name = "U16"}, 
            {.name = "U32"}, 
            {.name = "U64"},
            {.name = "I8"}, 
            {.name = "I16"}, 
            {.name = "I32"}, 
            {.name = "I64"}, 
            {.name = "F32"}, 
            {.name = "F64"}, 
            {.name = "UPtr"},
            {.name = "IPtr"}, 
            {.name = "String"}, 
            {.name = "Entity"}
        }
    });

    ecs_struct_init(world, &(ecs_struct_desc_t){
        .entity = ecs_id(EcsPrimitive),
        .members = {
            {.name = (char*)"kind", .type = primitive_kind}
        }
    });

    ecs_struct_init(world, &(ecs_struct_desc_t){
        .entity = ecs_id(EcsMember),
        .members = {
            {.name = (char*)"type", .type = ecs_id(ecs_entity_t)},
            {.name = (char*)"count", .type = ecs_id(ecs_i32_t)},
            {.name = (char*)"unit", .type = ecs_id(ecs_entity_t)},
            {.name = (char*)"offset", .type = ecs_id(ecs_i32_t)}
        }
    });

    ecs_struct_init(world, &(ecs_struct_desc_t){
        .entity = ecs_id(EcsArray),
        .members = {
            {.name = (char*)"type", .type = ecs_id(ecs_entity_t)},
            {.name = (char*)"count", .type = ecs_id(ecs_i32_t)},
        }
    });

    ecs_struct_init(world, &(ecs_struct_desc_t){
        .entity = ecs_id(EcsVector),
        .members = {
            {.name = (char*)"type", .type = ecs_id(ecs_entity_t)}
        }
    });

    ecs_entity_t ut = ecs_struct_init(world, &(ecs_struct_desc_t){
        .entity = ecs_entity(world, { .name = "unit_translation" }),
        .members = {
            {.name = (char*)"factor", .type = ecs_id(ecs_i32_t)},
            {.name = (char*)"power", .type = ecs_id(ecs_i32_t)}
        }
    });

    ecs_struct_init(world, &(ecs_struct_desc_t){
        .entity = ecs_id(EcsUnit),
        .members = {
            {.name = (char*)"symbol", .type = ecs_id(ecs_string_t)},
            {.name = (char*)"prefix", .type = ecs_id(ecs_entity_t)},
            {.name = (char*)"base", .type = ecs_id(ecs_entity_t)},
            {.name = (char*)"over", .type = ecs_id(ecs_entity_t)},
            {.name = (char*)"translation", .type = ut}
        }
    });

    ecs_struct_init(world, &(ecs_struct_desc_t){
        .entity = ecs_id(EcsUnitPrefix),
        .members = {
            {.name = (char*)"symbol", .type = ecs_id(ecs_string_t)},
            {.name = (char*)"translation", .type = ut}
        }
    });
}

#endif


#ifdef FLECS_META

static
const char* op_kind_str(
    ecs_meta_type_op_kind_t kind) 
{
    switch(kind) {

    case EcsOpEnum: return "Enum";
    case EcsOpBitmask: return "Bitmask";
    case EcsOpArray: return "Array";
    case EcsOpVector: return "Vector";
    case EcsOpPush: return "Push";
    case EcsOpPop: return "Pop";
    case EcsOpPrimitive: return "Primitive";
    case EcsOpBool: return "Bool";
    case EcsOpChar: return "Char";
    case EcsOpByte: return "Byte";
    case EcsOpU8: return "U8";
    case EcsOpU16: return "U16";
    case EcsOpU32: return "U32";
    case EcsOpU64: return "U64";
    case EcsOpI8: return "I8";
    case EcsOpI16: return "I16";
    case EcsOpI32: return "I32";
    case EcsOpI64: return "I64";
    case EcsOpF32: return "F32";
    case EcsOpF64: return "F64";
    case EcsOpUPtr: return "UPtr";
    case EcsOpIPtr: return "IPtr";
    case EcsOpString: return "String";
    case EcsOpEntity: return "Entity";
    default: return "<< invalid kind >>";
    }
}

/* Get current scope */
static
ecs_meta_scope_t* get_scope(
    const ecs_meta_cursor_t *cursor)
{
    ecs_check(cursor != NULL, ECS_INVALID_PARAMETER, NULL);
    return (ecs_meta_scope_t*)&cursor->scope[cursor->depth];
error:
    return NULL;
}

/* Get previous scope */
static
ecs_meta_scope_t* get_prev_scope(
    ecs_meta_cursor_t *cursor)
{
    ecs_check(cursor != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(cursor->depth > 0, ECS_INVALID_PARAMETER, NULL);
    return &cursor->scope[cursor->depth - 1];
error:
    return NULL;
}

/* Get current operation for scope */
static
ecs_meta_type_op_t* get_op(
    ecs_meta_scope_t *scope)
{
    return &scope->ops[scope->op_cur];
}

/* Get component for type in current scope */
static
const EcsComponent* get_component_ptr(
    const ecs_world_t *world,
    ecs_meta_scope_t *scope)
{
    const EcsComponent *comp = scope->comp;
    if (!comp) {
        comp = scope->comp = ecs_get(world, scope->type, EcsComponent);
        ecs_assert(comp != NULL, ECS_INTERNAL_ERROR, NULL);
    }
    return comp;
}

/* Get size for type in current scope */
static
ecs_size_t get_size(
    const ecs_world_t *world,
    ecs_meta_scope_t *scope)
{
    return get_component_ptr(world, scope)->size;
}

/* Get alignment for type in current scope */
static
ecs_size_t get_alignment(
    const ecs_world_t *world,
    ecs_meta_scope_t *scope)
{
    return get_component_ptr(world, scope)->alignment;
}

static
int32_t get_elem_count(
    ecs_meta_scope_t *scope)
{    
    if (scope->vector) {
        return ecs_vector_count(*(scope->vector));
    }

    ecs_meta_type_op_t *op = get_op(scope);
    return op->count;
}

/* Get pointer to current field/element */
static
ecs_meta_type_op_t* get_ptr(
    const ecs_world_t *world,
    ecs_meta_scope_t *scope)
{
    ecs_meta_type_op_t *op = get_op(scope);
    ecs_size_t size = get_size(world, scope);

    if (scope->vector) {
        ecs_size_t align = get_alignment(world, scope);
        ecs_vector_set_min_count_t(
            scope->vector, size, align, scope->elem_cur + 1);
        scope->ptr = ecs_vector_first_t(*(scope->vector), size, align);
    }

    return ECS_OFFSET(scope->ptr, size * scope->elem_cur + op->offset);
}

static
int push_type(
    const ecs_world_t *world,
    ecs_meta_scope_t *scope,
    ecs_entity_t type,
    void *ptr)
{
    const EcsMetaTypeSerialized *ser = ecs_get(
        world, type, EcsMetaTypeSerialized);
    if (ser == NULL) {
        char *str = ecs_id_str(world, type);
        ecs_err("cannot open scope for entity '%s' which is not a type", str);
        ecs_os_free(str);
        return -1;
    }

    scope[0] = (ecs_meta_scope_t) {
        .type = type,
        .ops = ecs_vector_first(ser->ops, ecs_meta_type_op_t),
        .op_count = ecs_vector_count(ser->ops),
        .ptr = ptr
    };

    return 0;
}

ecs_meta_cursor_t ecs_meta_cursor(
    const ecs_world_t *world,
    ecs_entity_t type,
    void *ptr)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(type != 0, ECS_INVALID_PARAMETER, NULL);
    ecs_check(ptr != NULL, ECS_INVALID_PARAMETER, NULL);

    ecs_meta_cursor_t result = {
        .world = world,
        .valid = true
    };

    if (push_type(world, result.scope, type, ptr) != 0) {
        result.valid = false;
    }

    return result;
error:
    return (ecs_meta_cursor_t){ 0 };
}

void* ecs_meta_get_ptr(
    ecs_meta_cursor_t *cursor)
{
    return get_ptr(cursor->world, get_scope(cursor));
}

int ecs_meta_next(
    ecs_meta_cursor_t *cursor)
{
    ecs_meta_scope_t *scope = get_scope(cursor);
    ecs_meta_type_op_t *op = get_op(scope);

    if (scope->is_collection) {
        scope->elem_cur ++;
        scope->op_cur = 0;
        if (scope->elem_cur >= get_elem_count(scope)) {
            ecs_err("out of collection bounds (%d)", scope->elem_cur);
            return -1;
        }
        
        return 0;
    }

    scope->op_cur += op->op_count;
    if (scope->op_cur >= scope->op_count) {
        ecs_err("out of bounds");
        return -1;
    }

    return 0;
}

int ecs_meta_elem(
    ecs_meta_cursor_t *cursor,
    int32_t elem)
{
    ecs_meta_scope_t *scope = get_scope(cursor);
    if (!scope->is_collection) {
        ecs_err("ecs_meta_elem can be used for collections only");
        return -1;
    }

    scope->elem_cur = elem;
    scope->op_cur = 0;

    if (scope->elem_cur >= get_elem_count(scope) || (scope->elem_cur < 0)) {
        ecs_err("out of collection bounds (%d)", scope->elem_cur);
        return -1;
    }
    
    return 0;
}

int ecs_meta_member(
    ecs_meta_cursor_t *cursor,
    const char *name)
{
    if (cursor->depth == 0) {
        ecs_err("cannot move to member in root scope");
        return -1;
    }

    ecs_meta_scope_t *prev_scope = get_prev_scope(cursor);
    ecs_meta_scope_t *scope = get_scope(cursor);
    ecs_meta_type_op_t *push_op = get_op(prev_scope);
    const ecs_world_t *world = cursor->world;

    ecs_assert(push_op->kind == EcsOpPush, ECS_INTERNAL_ERROR, NULL);
    
    if (!push_op->members) {
        ecs_err("cannot move to member '%s' for non-struct type", name);
        return -1;
    }

    const uint64_t *cur_ptr = flecs_name_index_find_ptr(push_op->members, name, 0, 0);
    if (!cur_ptr) {
        char *path = ecs_get_fullpath(world, scope->type);
        ecs_err("unknown member '%s' for type '%s'", name, path);
        ecs_os_free(path);
        return -1;
    }

    scope->op_cur = flecs_uto(int32_t, cur_ptr[0]);

    return 0;
}

int ecs_meta_push(
    ecs_meta_cursor_t *cursor)
{
    ecs_meta_scope_t *scope = get_scope(cursor);
    ecs_meta_type_op_t *op = get_op(scope);
    const ecs_world_t *world = cursor->world;

    if (cursor->depth == 0) {
        if (!cursor->is_primitive_scope) {
            if (op->kind > EcsOpScope) {
                cursor->is_primitive_scope = true;
                return 0;
            }
        }
    }

    void *ptr = get_ptr(world, scope);
    cursor->depth ++;
    ecs_check(cursor->depth < ECS_META_MAX_SCOPE_DEPTH, 
        ECS_INVALID_PARAMETER, NULL);

    ecs_meta_scope_t *next_scope = get_scope(cursor);

    /* If we're not already in an inline array and this operation is an inline
     * array, push a frame for the array.
     * Doing this first ensures that inline arrays take precedence over other
     * kinds of push operations, such as for a struct element type. */
    if (!scope->is_inline_array && op->count > 1 && !scope->is_collection) {
        /* Push a frame just for the element type, with inline_array = true */
        next_scope[0] = (ecs_meta_scope_t){
            .ops = op,
            .op_count = op->op_count,
            .ptr = scope->ptr,
            .type = op->type,
            .is_collection = true,
            .is_inline_array = true
        };

        /* With 'is_inline_array' set to true we ensure that we can never push
         * the same inline array twice */

        return 0;
    }

    switch(op->kind) {
    case EcsOpPush:
        next_scope[0] = (ecs_meta_scope_t) {
            .ops = &op[1],                /* op after push */
            .op_count = op->op_count - 1, /* don't include pop */
            .ptr = scope->ptr,
            .type = op->type
        };
        break;

    case EcsOpArray: {
        if (push_type(world, next_scope, op->type, ptr) != 0) {
            goto error;
        }

        const EcsArray *type_ptr = ecs_get(world, op->type, EcsArray);
        next_scope->type = type_ptr->type;
        next_scope->is_collection = true;
        break;
    }

    case EcsOpVector:
        next_scope->vector = ptr;
        if (push_type(world, next_scope, op->type, NULL) != 0) {
            goto error;
        }

        const EcsVector *type_ptr = ecs_get(world, op->type, EcsVector);
        next_scope->type = type_ptr->type;
        next_scope->is_collection = true;
        break;

    default: {
        char *path = ecs_get_fullpath(world, scope->type);
        ecs_err("invalid push for type '%s'", path);
        ecs_os_free(path);
        goto error;
    }
    }

    if (scope->is_collection) {
        next_scope[0].ptr = ECS_OFFSET(next_scope[0].ptr, 
            scope->elem_cur * get_size(world, scope));
    }

    return 0;
error:
    return -1;
}

int ecs_meta_pop(
    ecs_meta_cursor_t *cursor)
{
    if (cursor->is_primitive_scope) {
        cursor->is_primitive_scope = false;
        return 0;
    }

    ecs_meta_scope_t *scope = get_scope(cursor);
    cursor->depth --;
    if (cursor->depth < 0) {
        ecs_err("unexpected end of scope");
        return -1;
    }

    ecs_meta_scope_t *next_scope = get_scope(cursor);
    ecs_meta_type_op_t *op = get_op(next_scope);

    if (!scope->is_inline_array) {
        if (op->kind == EcsOpPush) {
            next_scope->op_cur += op->op_count - 1;

            /* push + op_count should point to the operation after pop */
            op = get_op(next_scope);
            ecs_assert(op->kind == EcsOpPop, ECS_INTERNAL_ERROR, NULL);
        } else if (op->kind == EcsOpArray || op->kind == EcsOpVector) {
            /* Collection type, nothing else to do */
        } else {
            /* should not have been able to push if the previous scope was not
             * a complex or collection type */
            ecs_assert(false, ECS_INTERNAL_ERROR, NULL);
        }
    } else {
        /* Make sure that this was an inline array */
        ecs_assert(next_scope->op_count > 1, ECS_INTERNAL_ERROR, NULL);
    }

    return 0;
}

bool ecs_meta_is_collection(
    const ecs_meta_cursor_t *cursor)
{
    ecs_meta_scope_t *scope = get_scope(cursor);
    return scope->is_collection;
}

ecs_entity_t ecs_meta_get_type(
    const ecs_meta_cursor_t *cursor)
{
    ecs_meta_scope_t *scope = get_scope(cursor);
    ecs_meta_type_op_t *op = get_op(scope);
    return op->type;
}

ecs_entity_t ecs_meta_get_unit(
    const ecs_meta_cursor_t *cursor)
{
    ecs_meta_scope_t *scope = get_scope(cursor);
    ecs_meta_type_op_t *op = get_op(scope);
    return op->unit;
}

const char* ecs_meta_get_member(
    const ecs_meta_cursor_t *cursor)
{
    ecs_meta_scope_t *scope = get_scope(cursor);
    ecs_meta_type_op_t *op = get_op(scope);
    return op->name;
}

/* Utility macros to let the compiler do the conversion work for us */
#define set_T(T, ptr, value)\
    ((T*)ptr)[0] = ((T)value)

#define case_T(kind, T, dst, src)\
case kind:\
    set_T(T, dst, src);\
    break

#define cases_T_float(dst, src)\
    case_T(EcsOpF32,  ecs_f32_t,  dst, src);\
    case_T(EcsOpF64,  ecs_f64_t,  dst, src)

#define cases_T_signed(dst, src)\
    case_T(EcsOpChar, ecs_char_t, dst, src);\
    case_T(EcsOpI8,   ecs_i8_t,   dst, src);\
    case_T(EcsOpI16,  ecs_i16_t,  dst, src);\
    case_T(EcsOpI32,  ecs_i32_t,  dst, src);\
    case_T(EcsOpI64,  ecs_i64_t,  dst, src);\
    case_T(EcsOpIPtr, ecs_iptr_t, dst, src)

#define cases_T_unsigned(dst, src)\
    case_T(EcsOpByte, ecs_byte_t, dst, src);\
    case_T(EcsOpU8,   ecs_u8_t,   dst, src);\
    case_T(EcsOpU16,  ecs_u16_t,  dst, src);\
    case_T(EcsOpU32,  ecs_u32_t,  dst, src);\
    case_T(EcsOpU64,  ecs_u64_t,  dst, src);\
    case_T(EcsOpUPtr, ecs_uptr_t, dst, src);\

#define cases_T_bool(dst, src)\
case EcsOpBool:\
    set_T(ecs_bool_t, dst, value != 0);\
    break

static
void conversion_error(
    ecs_meta_cursor_t *cursor,
    ecs_meta_type_op_t *op,
    const char *from)
{
    char *path = ecs_get_fullpath(cursor->world, op->type);
    ecs_err("unsupported conversion from %s to '%s'", from, path);
    ecs_os_free(path);
}

int ecs_meta_set_bool(
    ecs_meta_cursor_t *cursor,
    bool value)
{
    ecs_meta_scope_t *scope = get_scope(cursor);
    ecs_meta_type_op_t *op = get_op(scope);
    void *ptr = get_ptr(cursor->world, scope);

    switch(op->kind) {
    cases_T_bool(ptr, value);
    cases_T_unsigned(ptr, value);
    default:
        conversion_error(cursor, op, "bool");
        return -1;
    }

    return 0;
}

int ecs_meta_set_char(
    ecs_meta_cursor_t *cursor,
    char value)
{
    ecs_meta_scope_t *scope = get_scope(cursor);
    ecs_meta_type_op_t *op = get_op(scope);
    void *ptr = get_ptr(cursor->world, scope);

    switch(op->kind) {
    cases_T_bool(ptr, value);
    cases_T_signed(ptr, value);
    default:
        conversion_error(cursor, op, "char");
        return -1;
    }

    return 0;
}

int ecs_meta_set_int(
    ecs_meta_cursor_t *cursor,
    int64_t value)
{
    ecs_meta_scope_t *scope = get_scope(cursor);
    ecs_meta_type_op_t *op = get_op(scope);
    void *ptr = get_ptr(cursor->world, scope);

    switch(op->kind) {
    cases_T_bool(ptr, value);
    cases_T_signed(ptr, value);
    cases_T_float(ptr, value);
    default: {
        conversion_error(cursor, op, "int");
        return -1;
    }
    }

    return 0;
}

int ecs_meta_set_uint(
    ecs_meta_cursor_t *cursor,
    uint64_t value)
{
    ecs_meta_scope_t *scope = get_scope(cursor);
    ecs_meta_type_op_t *op = get_op(scope);
    void *ptr = get_ptr(cursor->world, scope);

    switch(op->kind) {
    cases_T_bool(ptr, value);
    cases_T_unsigned(ptr, value);
    cases_T_float(ptr, value);
    case EcsOpEntity:
        set_T(ecs_entity_t, ptr, value);
        break;
    default:
        conversion_error(cursor, op, "uint");
        return -1;
    }

    return 0;
}

int ecs_meta_set_float(
    ecs_meta_cursor_t *cursor,
    double value)
{
    ecs_meta_scope_t *scope = get_scope(cursor);
    ecs_meta_type_op_t *op = get_op(scope);
    void *ptr = get_ptr(cursor->world, scope);

    switch(op->kind) {
    cases_T_bool(ptr, value);
    cases_T_signed(ptr, value);
    cases_T_unsigned(ptr, value);
    cases_T_float(ptr, value);
    default:
        conversion_error(cursor, op, "float");
        return -1;
    }

    return 0;
}

static
int add_bitmask_constant(
    ecs_meta_cursor_t *cursor, 
    ecs_meta_type_op_t *op,
    void *out, 
    const char *value)
{
    ecs_assert(op->type != 0, ECS_INTERNAL_ERROR, NULL);

    if (!ecs_os_strcmp(value, "0")) {
        return 0;
    }

    ecs_entity_t c = ecs_lookup_child(cursor->world, op->type, value);
    if (!c) {
        char *path = ecs_get_fullpath(cursor->world, op->type);
        ecs_err("unresolved bitmask constant '%s' for type '%s'", value, path);
        ecs_os_free(path);
        return -1;
    }

    const ecs_u32_t *v = ecs_get_pair_object(
        cursor->world, c, EcsConstant, ecs_u32_t);
    if (v == NULL) {
        char *path = ecs_get_fullpath(cursor->world, op->type);
        ecs_err("'%s' is not an bitmask constant for type '%s'", value, path);
        ecs_os_free(path);
        return -1;
    }

    *(ecs_u32_t*)out |= v[0];

    return 0;
}

static
int parse_bitmask(
    ecs_meta_cursor_t *cursor, 
    ecs_meta_type_op_t *op,
    void *out, 
    const char *value)
{
    char token[ECS_MAX_TOKEN_SIZE];

    const char *prev = value, *ptr = value;

    *(ecs_u32_t*)out = 0;

    while ((ptr = strchr(ptr, '|'))) {
        ecs_os_memcpy(token, prev, ptr - prev);
        token[ptr - prev] = '\0';
        if (add_bitmask_constant(cursor, op, out, token) != 0) {
            return -1;
        }

        ptr ++;
        prev = ptr;
    }

    if (add_bitmask_constant(cursor, op, out, prev) != 0) {
        return -1;
    }

    return 0;
}

int ecs_meta_set_string(
    ecs_meta_cursor_t *cursor,
    const char *value)
{
    ecs_meta_scope_t *scope = get_scope(cursor);
    ecs_meta_type_op_t *op = get_op(scope);
    void *ptr = get_ptr(cursor->world, scope);

    switch(op->kind) {
    case EcsOpBool:
        if (!ecs_os_strcmp(value, "true")) {
            set_T(ecs_bool_t, ptr, true);
        } else if (!ecs_os_strcmp(value, "false")) {
            set_T(ecs_bool_t, ptr, false);
        } else {
            ecs_err("invalid value for boolean '%s'", value);
            return -1;
        }
        break;
    case EcsOpI8:
    case EcsOpU8:
    case EcsOpChar:
    case EcsOpByte:
        set_T(ecs_i8_t, ptr, atol(value));
        break;
    case EcsOpI16:
    case EcsOpU16:
        set_T(ecs_i16_t, ptr, atol(value));
        break;
    case EcsOpI32:
    case EcsOpU32:
        set_T(ecs_i32_t, ptr, atol(value));
        break;
    case EcsOpI64:
    case EcsOpU64:
        set_T(ecs_i64_t, ptr, atol(value));
        break;
    case EcsOpIPtr:
    case EcsOpUPtr:
        set_T(ecs_iptr_t, ptr, atol(value));
        break;
    case EcsOpF32:
        set_T(ecs_f32_t, ptr, atof(value));
        break;
    case EcsOpF64:
        set_T(ecs_f64_t, ptr, atof(value));
        break;
    case EcsOpString: {
        ecs_os_free(*(char**)ptr);
        char *result = ecs_os_strdup(value);
        set_T(ecs_string_t, ptr, result);
        break;
    }
    case EcsOpEnum: {
        ecs_assert(op->type != 0, ECS_INTERNAL_ERROR, NULL);
        ecs_entity_t c = ecs_lookup_child(cursor->world, op->type, value);
        if (!c) {
            char *path = ecs_get_fullpath(cursor->world, op->type);
            ecs_err("unresolved enum constant '%s' for type '%s'", value, path);
            ecs_os_free(path);
            return -1;
        }

        const ecs_i32_t *v = ecs_get_pair_object(
            cursor->world, c, EcsConstant, ecs_i32_t);
        if (v == NULL) {
            char *path = ecs_get_fullpath(cursor->world, op->type);
            ecs_err("'%s' is not an enum constant for type '%s'", value, path);
            ecs_os_free(path);
            return -1;
        }

        set_T(ecs_i32_t, ptr, v[0]);
        break;
    }
    case EcsOpBitmask:
        if (parse_bitmask(cursor, op, ptr, value) != 0) {
            return -1;
        }
        break;
    case EcsOpEntity: {
        ecs_entity_t e = 0;

        if (ecs_os_strcmp(value, "0")) {
            if (cursor->lookup_action) {
                e = cursor->lookup_action(
                    cursor->world, value, 
                    cursor->lookup_ctx);
            } else {
                e = ecs_lookup_path(cursor->world, 0, value);
            }

            if (!e) {
                ecs_err("unresolved entity identifier '%s'", value);
                return -1;
            }
        }

        set_T(ecs_entity_t, ptr, e);
        break;
    }
    case EcsOpPop:
        ecs_err("excess element '%s' in scope", value);
        return -1;
    default:
        ecs_err("unsupported conversion from string '%s' to '%s'", 
            value, op_kind_str(op->kind));
        return -1;
    }

    return 0;
}

int ecs_meta_set_string_literal(
    ecs_meta_cursor_t *cursor,
    const char *value)
{
    ecs_meta_scope_t *scope = get_scope(cursor);
    ecs_meta_type_op_t *op = get_op(scope);
    void *ptr = get_ptr(cursor->world, scope);

    ecs_size_t len = ecs_os_strlen(value);
    if (value[0] != '\"' || value[len - 1] != '\"') {
        ecs_err("invalid string literal '%s'", value);
        return -1;
    }

    switch(op->kind) {
    case EcsOpChar:
        set_T(ecs_char_t, ptr, value[1]);
        break;
    
    default:
    case EcsOpEntity:
    case EcsOpString:
        len -= 2;

        char *result = ecs_os_malloc(len + 1);
        ecs_os_memcpy(result, value + 1, len);
        result[len] = '\0';

        if (ecs_meta_set_string(cursor, result)) {
            ecs_os_free(result);
            return -1;
        }

        ecs_os_free(result);

        break;
    }

    return 0;
}

int ecs_meta_set_entity(
    ecs_meta_cursor_t *cursor,
    ecs_entity_t value)
{
    ecs_meta_scope_t *scope = get_scope(cursor);
    ecs_meta_type_op_t *op = get_op(scope);
    void *ptr = get_ptr(cursor->world, scope);

    switch(op->kind) {
    case EcsOpEntity:
        set_T(ecs_entity_t, ptr, value);
        break;
    default:
        conversion_error(cursor, op, "entity");
        return -1;
    }

    return 0;
}

int ecs_meta_set_null(
    ecs_meta_cursor_t *cursor)
{
    ecs_meta_scope_t *scope = get_scope(cursor);
    ecs_meta_type_op_t *op = get_op(scope);
    void *ptr = get_ptr(cursor->world, scope);
    switch (op->kind) {
    case EcsOpString:
        ecs_os_free(*(char**)ptr);
        set_T(ecs_string_t, ptr, NULL);
        break;
    default:
        conversion_error(cursor, op, "null");
        return -1;
    }

    return 0;
}

bool ecs_meta_get_bool(
    const ecs_meta_cursor_t *cursor)
{
    ecs_meta_scope_t *scope = get_scope(cursor);
    ecs_meta_type_op_t *op = get_op(scope);
    void *ptr = get_ptr(cursor->world, scope);
    switch(op->kind) {
    case EcsOpBool: return *(ecs_bool_t*)ptr;
    case EcsOpI8:   return *(ecs_i8_t*)ptr != 0;
    case EcsOpU8:   return *(ecs_u8_t*)ptr != 0;
    case EcsOpChar: return *(ecs_char_t*)ptr != 0;
    case EcsOpByte: return *(ecs_u8_t*)ptr != 0;
    case EcsOpI16:  return *(ecs_i16_t*)ptr != 0;
    case EcsOpU16:  return *(ecs_u16_t*)ptr != 0;
    case EcsOpI32:  return *(ecs_i32_t*)ptr != 0;
    case EcsOpU32:  return *(ecs_u32_t*)ptr != 0;
    case EcsOpI64:  return *(ecs_i64_t*)ptr != 0;
    case EcsOpU64:  return *(ecs_u64_t*)ptr != 0;
    case EcsOpIPtr: return *(ecs_iptr_t*)ptr != 0;
    case EcsOpUPtr: return *(ecs_uptr_t*)ptr != 0;
    case EcsOpF32:  return *(ecs_f32_t*)ptr != 0;
    case EcsOpF64:  return *(ecs_f64_t*)ptr != 0;
    case EcsOpString: return *(const char**)ptr != NULL;
    case EcsOpEnum: return *(ecs_i32_t*)ptr != 0;
    case EcsOpBitmask: return *(ecs_u32_t*)ptr != 0;
    case EcsOpEntity: return *(ecs_entity_t*)ptr != 0;
    default: ecs_throw(ECS_INVALID_PARAMETER, 
                "invalid element for bool");
    }
error:
    return 0;
}

char ecs_meta_get_char(
    const ecs_meta_cursor_t *cursor)
{
    ecs_meta_scope_t *scope = get_scope(cursor);
    ecs_meta_type_op_t *op = get_op(scope);
    void *ptr = get_ptr(cursor->world, scope);
    switch(op->kind) {
    case EcsOpChar: return *(ecs_char_t*)ptr != 0;
    default: ecs_throw(ECS_INVALID_PARAMETER, 
                "invalid element for char");
    }
error:
    return 0;
}

int64_t ecs_meta_get_int(
    const ecs_meta_cursor_t *cursor)
{
    ecs_meta_scope_t *scope = get_scope(cursor);
    ecs_meta_type_op_t *op = get_op(scope);
    void *ptr = get_ptr(cursor->world, scope);
    switch(op->kind) {
    case EcsOpBool: return *(ecs_bool_t*)ptr;
    case EcsOpI8:   return *(ecs_i8_t*)ptr;
    case EcsOpU8:   return *(ecs_u8_t*)ptr;
    case EcsOpChar: return *(ecs_char_t*)ptr;
    case EcsOpByte: return *(ecs_u8_t*)ptr;
    case EcsOpI16:  return *(ecs_i16_t*)ptr;
    case EcsOpU16:  return *(ecs_u16_t*)ptr;
    case EcsOpI32:  return *(ecs_i32_t*)ptr;
    case EcsOpU32:  return *(ecs_u32_t*)ptr;
    case EcsOpI64:  return *(ecs_i64_t*)ptr;
    case EcsOpU64:  return flecs_uto(int64_t, *(ecs_u64_t*)ptr);
    case EcsOpIPtr: return *(ecs_iptr_t*)ptr;
    case EcsOpUPtr: return flecs_uto(int64_t, *(ecs_uptr_t*)ptr);
    case EcsOpF32:  return (int64_t)*(ecs_f32_t*)ptr;
    case EcsOpF64:  return (int64_t)*(ecs_f64_t*)ptr;
    case EcsOpString: return atoi(*(const char**)ptr);
    case EcsOpEnum: return *(ecs_i32_t*)ptr;
    case EcsOpBitmask: return *(ecs_u32_t*)ptr;
    case EcsOpEntity: 
        ecs_throw(ECS_INVALID_PARAMETER, 
            "invalid conversion from entity to int");
        break;
    default: ecs_throw(ECS_INVALID_PARAMETER, "invalid element for int");
    }
error:
    return 0;
}

uint64_t ecs_meta_get_uint(
    const ecs_meta_cursor_t *cursor)
{
    ecs_meta_scope_t *scope = get_scope(cursor);
    ecs_meta_type_op_t *op = get_op(scope);
    void *ptr = get_ptr(cursor->world, scope);
    switch(op->kind) {
    case EcsOpBool: return *(ecs_bool_t*)ptr;
    case EcsOpI8:   return flecs_ito(uint64_t, *(ecs_i8_t*)ptr);
    case EcsOpU8:   return *(ecs_u8_t*)ptr;
    case EcsOpChar: return flecs_ito(uint64_t, *(ecs_char_t*)ptr);
    case EcsOpByte: return flecs_ito(uint64_t, *(ecs_u8_t*)ptr);
    case EcsOpI16:  return flecs_ito(uint64_t, *(ecs_i16_t*)ptr);
    case EcsOpU16:  return *(ecs_u16_t*)ptr;
    case EcsOpI32:  return flecs_ito(uint64_t, *(ecs_i32_t*)ptr);
    case EcsOpU32:  return *(ecs_u32_t*)ptr;
    case EcsOpI64:  return flecs_ito(uint64_t, *(ecs_i64_t*)ptr);
    case EcsOpU64:  return *(ecs_u64_t*)ptr;
    case EcsOpIPtr: return flecs_ito(uint64_t, *(ecs_i64_t*)ptr);
    case EcsOpUPtr: return *(ecs_uptr_t*)ptr;
    case EcsOpF32:  return flecs_ito(uint64_t, *(ecs_f32_t*)ptr);
    case EcsOpF64:  return flecs_ito(uint64_t, *(ecs_f64_t*)ptr);
    case EcsOpString: return flecs_ito(uint64_t, atoi(*(const char**)ptr));
    case EcsOpEnum: return flecs_ito(uint64_t, *(ecs_i32_t*)ptr);
    case EcsOpBitmask: return *(ecs_u32_t*)ptr;
    case EcsOpEntity: return *(ecs_entity_t*)ptr;
    default: ecs_throw(ECS_INVALID_PARAMETER, "invalid element for uint");
    }
error:
    return 0;
}

double ecs_meta_get_float(
    const ecs_meta_cursor_t *cursor)
{
    ecs_meta_scope_t *scope = get_scope(cursor);
    ecs_meta_type_op_t *op = get_op(scope);
    void *ptr = get_ptr(cursor->world, scope);
    switch(op->kind) {
    case EcsOpBool: return *(ecs_bool_t*)ptr;
    case EcsOpI8:   return *(ecs_i8_t*)ptr;
    case EcsOpU8:   return *(ecs_u8_t*)ptr;
    case EcsOpChar: return *(ecs_char_t*)ptr;
    case EcsOpByte: return *(ecs_u8_t*)ptr;
    case EcsOpI16:  return *(ecs_i16_t*)ptr;
    case EcsOpU16:  return *(ecs_u16_t*)ptr;
    case EcsOpI32:  return *(ecs_i32_t*)ptr;
    case EcsOpU32:  return *(ecs_u32_t*)ptr;
    case EcsOpI64:  return (double)*(ecs_i64_t*)ptr;
    case EcsOpU64:  return (double)*(ecs_u64_t*)ptr;
    case EcsOpIPtr: return (double)*(ecs_iptr_t*)ptr;
    case EcsOpUPtr: return (double)*(ecs_uptr_t*)ptr;
    case EcsOpF32:  return (double)*(ecs_f32_t*)ptr;
    case EcsOpF64:  return *(ecs_f64_t*)ptr;
    case EcsOpString: return atof(*(const char**)ptr);
    case EcsOpEnum: return *(ecs_i32_t*)ptr;
    case EcsOpBitmask: return *(ecs_u32_t*)ptr;
    case EcsOpEntity: 
        ecs_throw(ECS_INVALID_PARAMETER, 
            "invalid conversion from entity to float");
        break;
    default: ecs_throw(ECS_INVALID_PARAMETER, "invalid element for float");
    }
error:
    return 0;
}

const char* ecs_meta_get_string(
    const ecs_meta_cursor_t *cursor)
{
    ecs_meta_scope_t *scope = get_scope(cursor);
    ecs_meta_type_op_t *op = get_op(scope);
    void *ptr = get_ptr(cursor->world, scope);
    switch(op->kind) {
    case EcsOpString: return *(const char**)ptr;
    default: ecs_throw(ECS_INVALID_PARAMETER, "invalid element for string");
    }
error:
    return 0;
}

ecs_entity_t ecs_meta_get_entity(
    const ecs_meta_cursor_t *cursor)
{
    ecs_meta_scope_t *scope = get_scope(cursor);
    ecs_meta_type_op_t *op = get_op(scope);
    void *ptr = get_ptr(cursor->world, scope);
    switch(op->kind) {
    case EcsOpEntity: return *(ecs_entity_t*)ptr;
    default: ecs_throw(ECS_INVALID_PARAMETER, "invalid element for entity");
    }
error:
    return 0;
}

#endif



#ifdef FLECS_EXPR

static
int expr_ser_type(
    const ecs_world_t *world,
    ecs_vector_t *ser, 
    const void *base, 
    ecs_strbuf_t *str);

static
int expr_ser_type_ops(
    const ecs_world_t *world,
    ecs_meta_type_op_t *ops,
    int32_t op_count,
    const void *base, 
    ecs_strbuf_t *str,
    int32_t in_array);

static
int expr_ser_type_op(
    const ecs_world_t *world,
    ecs_meta_type_op_t *op, 
    const void *base,
    ecs_strbuf_t *str);

static
ecs_primitive_kind_t expr_op_to_primitive_kind(ecs_meta_type_op_kind_t kind) {
    return kind - EcsOpPrimitive;
}

/* Serialize a primitive value */
static
int expr_ser_primitive(
    const ecs_world_t *world,
    ecs_primitive_kind_t kind,
    const void *base, 
    ecs_strbuf_t *str) 
{
    const char *bool_str[] = { "false", "true" };

    switch(kind) {
    case EcsBool:
        ecs_strbuf_appendstr(str, bool_str[(int)*(bool*)base]);
        break;
    case EcsChar: {
        char chbuf[3];
        char ch = *(char*)base;
        if (ch) {
            ecs_chresc(chbuf, *(char*)base, '"');
            ecs_strbuf_appendstrn(str, "\"", 1);
            ecs_strbuf_appendstr(str, chbuf);
            ecs_strbuf_appendstrn(str, "\"", 1);
        } else {
            ecs_strbuf_appendstr(str, "0");
        }
        break;
    }
    case EcsByte:
        ecs_strbuf_append(str, "%u", *(uint8_t*)base);
        break;
    case EcsU8:
        ecs_strbuf_append(str, "%u", *(uint8_t*)base);
        break;
    case EcsU16:
        ecs_strbuf_append(str, "%u", *(uint16_t*)base);
        break;
    case EcsU32:
        ecs_strbuf_append(str, "%u", *(uint32_t*)base);
        break;
    case EcsU64:
        ecs_strbuf_append(str, "%llu", *(uint64_t*)base);
        break;
    case EcsI8:
        ecs_strbuf_append(str, "%d", *(int8_t*)base);
        break;
    case EcsI16:
        ecs_strbuf_append(str, "%d", *(int16_t*)base);
        break;
    case EcsI32:
        ecs_strbuf_append(str, "%d", *(int32_t*)base);
        break;
    case EcsI64:
        ecs_strbuf_append(str, "%lld", *(int64_t*)base);
        break;
    case EcsF32:
        ecs_strbuf_appendflt(str, (double)*(float*)base, 0);
        break;
    case EcsF64:
        ecs_strbuf_appendflt(str, *(double*)base, 0);
        break;
    case EcsIPtr:
        ecs_strbuf_append(str, "%i", *(intptr_t*)base);
        break;
    case EcsUPtr:
        ecs_strbuf_append(str, "%u", *(uintptr_t*)base);
        break;
    case EcsString: {
        char *value = *(char**)base;
        if (value) {
            ecs_size_t length = ecs_stresc(NULL, 0, '"', value);
            if (length == ecs_os_strlen(value)) {
                ecs_strbuf_appendstrn(str, "\"", 1);
                ecs_strbuf_appendstr(str, value);
                ecs_strbuf_appendstrn(str, "\"", 1);
            } else {
                char *out = ecs_os_malloc(length + 3);
                ecs_stresc(out + 1, length, '"', value);
                out[0] = '"';
                out[length + 1] = '"';
                out[length + 2] = '\0';
                ecs_strbuf_appendstr_zerocpy(str, out);
            }
        } else {
            ecs_strbuf_appendstr(str, "null");
        }
        break;
    }
    case EcsEntity: {
        ecs_entity_t e = *(ecs_entity_t*)base;
        if (!e) {
            ecs_strbuf_appendstr(str, "0");
        } else {
            char *path = ecs_get_fullpath(world, e);
            ecs_assert(path != NULL, ECS_INTERNAL_ERROR, NULL);
            ecs_strbuf_appendstr(str, path);
            ecs_os_free(path);
        }
        break;
    }
    default:
        ecs_err("invalid primitive kind");
        return -1;
    }

    return 0;
}

/* Serialize enumeration */
static
int expr_ser_enum(
    const ecs_world_t *world,
    ecs_meta_type_op_t *op, 
    const void *base, 
    ecs_strbuf_t *str) 
{
    const EcsEnum *enum_type = ecs_get(world, op->type, EcsEnum);
    ecs_check(enum_type != NULL, ECS_INVALID_PARAMETER, NULL);

    int32_t value = *(int32_t*)base;
    
    /* Enumeration constants are stored in a map that is keyed on the
     * enumeration value. */
    ecs_enum_constant_t *constant = ecs_map_get(
        enum_type->constants, ecs_enum_constant_t, value);
    if (!constant) {
        char *path = ecs_get_fullpath(world, op->type);
        ecs_err("value %d is not valid for enum type '%s'", value, path);
        ecs_os_free(path);
        goto error;
    }

    ecs_strbuf_appendstr(str, ecs_get_name(world, constant->constant));

    return 0;
error:
    return -1;
}

/* Serialize bitmask */
static
int expr_ser_bitmask(
    const ecs_world_t *world,
    ecs_meta_type_op_t *op, 
    const void *ptr, 
    ecs_strbuf_t *str) 
{
    const EcsBitmask *bitmask_type = ecs_get(world, op->type, EcsBitmask);
    ecs_check(bitmask_type != NULL, ECS_INVALID_PARAMETER, NULL);

    uint32_t value = *(uint32_t*)ptr;
    ecs_map_key_t key;
    ecs_bitmask_constant_t *constant;
    int count = 0;

    ecs_strbuf_list_push(str, "", "|");

    /* Multiple flags can be set at a given time. Iterate through all the flags
     * and append the ones that are set. */
    ecs_map_iter_t it = ecs_map_iter(bitmask_type->constants);
    while ((constant = ecs_map_next(&it, ecs_bitmask_constant_t, &key))) {
        if ((value & key) == key) {
            ecs_strbuf_list_appendstr(str, 
                ecs_get_name(world, constant->constant));
            count ++;
            value -= (uint32_t)key;
        }
    }

    if (value != 0) {
        /* All bits must have been matched by a constant */
        char *path = ecs_get_fullpath(world, op->type);
        ecs_err(
            "value for bitmask %s contains bits (%u) that cannot be mapped to constant", 
            path, value);
        ecs_os_free(path);
        goto error;
    }

    if (!count) {
        ecs_strbuf_list_appendstr(str, "0");
    }

    ecs_strbuf_list_pop(str, "");

    return 0;
error:
    return -1;
}

/* Serialize elements of a contiguous array */
static
int expr_ser_elements(
    const ecs_world_t *world,
    ecs_meta_type_op_t *ops, 
    int32_t op_count,
    const void *base, 
    int32_t elem_count, 
    int32_t elem_size,
    ecs_strbuf_t *str)
{
    ecs_strbuf_list_push(str, "[", ", ");

    const void *ptr = base;

    int i;
    for (i = 0; i < elem_count; i ++) {
        ecs_strbuf_list_next(str);
        if (expr_ser_type_ops(world, ops, op_count, ptr, str, 1)) {
            return -1;
        }
        ptr = ECS_OFFSET(ptr, elem_size);
    }

    ecs_strbuf_list_pop(str, "]");

    return 0;
}

static
int expr_ser_type_elements(
    const ecs_world_t *world,
    ecs_entity_t type, 
    const void *base, 
    int32_t elem_count, 
    ecs_strbuf_t *str)
{
    const EcsMetaTypeSerialized *ser = ecs_get(
        world, type, EcsMetaTypeSerialized);
    ecs_assert(ser != NULL, ECS_INTERNAL_ERROR, NULL);

    const EcsComponent *comp = ecs_get(world, type, EcsComponent);
    ecs_assert(comp != NULL, ECS_INTERNAL_ERROR, NULL);

    ecs_meta_type_op_t *ops = ecs_vector_first(ser->ops, ecs_meta_type_op_t);
    int32_t op_count = ecs_vector_count(ser->ops);

    return expr_ser_elements(
        world, ops, op_count, base, elem_count, comp->size, str);
}

/* Serialize array */
static
int expr_ser_array(
    const ecs_world_t *world,
    ecs_meta_type_op_t *op, 
    const void *ptr, 
    ecs_strbuf_t *str) 
{
    const EcsArray *a = ecs_get(world, op->type, EcsArray);
    ecs_assert(a != NULL, ECS_INTERNAL_ERROR, NULL);

    return expr_ser_type_elements(
        world, a->type, ptr, a->count, str);
}

/* Serialize vector */
static
int expr_ser_vector(
    const ecs_world_t *world,
    ecs_meta_type_op_t *op, 
    const void *base, 
    ecs_strbuf_t *str) 
{
    ecs_vector_t *value = *(ecs_vector_t**)base;
    if (!value) {
        ecs_strbuf_appendstr(str, "null");
        return 0;
    }

    const EcsVector *v = ecs_get(world, op->type, EcsVector);
    ecs_assert(v != NULL, ECS_INTERNAL_ERROR, NULL);

    const EcsComponent *comp = ecs_get(world, v->type, EcsComponent);
    ecs_assert(comp != NULL, ECS_INTERNAL_ERROR, NULL);

    int32_t count = ecs_vector_count(value);
    void *array = ecs_vector_first_t(value, comp->size, comp->alignment);

    /* Serialize contiguous buffer of vector */
    return expr_ser_type_elements(world, v->type, array, count, str);
}

/* Forward serialization to the different type kinds */
static
int expr_ser_type_op(
    const ecs_world_t *world,
    ecs_meta_type_op_t *op, 
    const void *ptr,
    ecs_strbuf_t *str) 
{
    switch(op->kind) {
    case EcsOpPush:
    case EcsOpPop:
        /* Should not be parsed as single op */
        ecs_throw(ECS_INVALID_PARAMETER, NULL);
        break;
    case EcsOpEnum:
        if (expr_ser_enum(world, op, ECS_OFFSET(ptr, op->offset), str)) {
            goto error;
        }
        break;
    case EcsOpBitmask:
        if (expr_ser_bitmask(world, op, ECS_OFFSET(ptr, op->offset), str)) {
            goto error;
        }
        break;
    case EcsOpArray:
        if (expr_ser_array(world, op, ECS_OFFSET(ptr, op->offset), str)) {
            goto error;
        }
        break;
    case EcsOpVector:
        if (expr_ser_vector(world, op, ECS_OFFSET(ptr, op->offset), str)) {
            goto error;
        }
        break;
    default:
        if (expr_ser_primitive(world, expr_op_to_primitive_kind(op->kind), 
            ECS_OFFSET(ptr, op->offset), str)) 
        {
            /* Unknown operation */
            ecs_err("unknown serializer operation kind (%d)", op->kind);
            goto error;
        }
        break;
    }

    return 0;
error:
    return -1;
}

/* Iterate over a slice of the type ops array */
static
int expr_ser_type_ops(
    const ecs_world_t *world,
    ecs_meta_type_op_t *ops,
    int32_t op_count,
    const void *base,
    ecs_strbuf_t *str,
    int32_t in_array) 
{
    for (int i = 0; i < op_count; i ++) {
        ecs_meta_type_op_t *op = &ops[i];

        if (in_array <= 0) {
            if (op->name) {
                ecs_strbuf_list_next(str);
                ecs_strbuf_append(str, "%s: ", op->name);
            }

            int32_t elem_count = op->count;
            if (elem_count > 1) {
                /* Serialize inline array */
                if (expr_ser_elements(world, op, op->op_count, base,
                    elem_count, op->size, str))
                {
                    return -1;
                }

                i += op->op_count - 1;
                continue;
            }
        }

        switch(op->kind) {
        case EcsOpPush:
            ecs_strbuf_list_push(str, "{", ", ");
            in_array --;
            break;
        case EcsOpPop:
            ecs_strbuf_list_pop(str, "}");
            in_array ++;
            break;
        default:
            if (expr_ser_type_op(world, op, base, str)) {
                goto error;
            }
            break;
        }
    }

    return 0;
error:
    return -1;
}

/* Iterate over the type ops of a type */
static
int expr_ser_type(
    const ecs_world_t *world,
    ecs_vector_t *v_ops,
    const void *base, 
    ecs_strbuf_t *str) 
{
    ecs_meta_type_op_t *ops = ecs_vector_first(v_ops, ecs_meta_type_op_t);
    int32_t count = ecs_vector_count(v_ops);
    return expr_ser_type_ops(world, ops, count, base, str, 0);
}

int ecs_ptr_to_expr_buf(
    const ecs_world_t *world,
    ecs_entity_t type,
    const void *ptr,
    ecs_strbuf_t *buf_out)
{
    const EcsMetaTypeSerialized *ser = ecs_get(
        world, type, EcsMetaTypeSerialized);
    if (ser == NULL) {
        char *path = ecs_get_fullpath(world, type);
        ecs_err("cannot serialize value for type '%s'", path);
        ecs_os_free(path);
        goto error;
    }

    if (expr_ser_type(world, ser->ops, ptr, buf_out)) {
        goto error;
    }

    return 0;
error:
    return -1;
}

char* ecs_ptr_to_expr(
    const ecs_world_t *world, 
    ecs_entity_t type, 
    const void* ptr)
{
    ecs_strbuf_t str = ECS_STRBUF_INIT;

    if (ecs_ptr_to_expr_buf(world, type, ptr, &str) != 0) {
        ecs_strbuf_reset(&str);
        return NULL;
    }

    return ecs_strbuf_get(&str);
}

int ecs_primitive_to_expr_buf(
    const ecs_world_t *world,
    ecs_primitive_kind_t kind,
    const void *base, 
    ecs_strbuf_t *str)
{
    return expr_ser_primitive(world, kind, base, str);
}

#endif



#ifdef FLECS_EXPR

char* ecs_chresc(
    char *out, 
    char in, 
    char delimiter) 
{
    char *bptr = out;
    switch(in) {
    case '\a':
        *bptr++ = '\\';
        *bptr = 'a';
        break;
    case '\b':
        *bptr++ = '\\';
        *bptr = 'b';
        break;
    case '\f':
        *bptr++ = '\\';
        *bptr = 'f';
        break;
    case '\n':
        *bptr++ = '\\';
        *bptr = 'n';
        break;
    case '\r':
        *bptr++ = '\\';
        *bptr = 'r';
        break;
    case '\t':
        *bptr++ = '\\';
        *bptr = 't';
        break;
    case '\v':
        *bptr++ = '\\';
        *bptr = 'v';
        break;
    case '\\':
        *bptr++ = '\\';
        *bptr = '\\';
        break;
    default:
        if (in == delimiter) {
            *bptr++ = '\\';
            *bptr = delimiter;
        } else {
            *bptr = in;
        }
        break;
    }

    *(++bptr) = '\0';

    return bptr;
}

const char* ecs_chrparse(
    const char *in, 
    char *out) 
{
    const char *result = in + 1;
    char ch;

    if (in[0] == '\\') {
        result ++;

        switch(in[1]) {
        case 'a':
            ch = '\a';
            break;
        case 'b':
            ch = '\b';
            break;
        case 'f':
            ch = '\f';
            break;
        case 'n':
            ch = '\n';
            break;
        case 'r':
            ch = '\r';
            break;
        case 't':
            ch = '\t';
            break;
        case 'v':
            ch = '\v';
            break;
        case '\\':
            ch = '\\';
            break;
        case '"':
            ch = '"';
            break;
        case '0':
            ch = '\0';
            break;
        case ' ':
            ch = ' ';
            break;
        case '$':
            ch = '$';
            break;
        default:
            goto error;
        }
    } else {
        ch = in[0];
    }

    if (out) {
        *out = ch;
    }

    return result;
error:
    return NULL;
}

ecs_size_t ecs_stresc(
    char *out, 
    ecs_size_t n, 
    char delimiter, 
    const char *in) 
{
    const char *ptr = in;
    char ch, *bptr = out, buff[3];
    ecs_size_t written = 0;
    while ((ch = *ptr++)) {
        if ((written += (ecs_size_t)(ecs_chresc(
            buff, ch, delimiter) - buff)) <= n) 
        {
            /* If size != 0, an out buffer must be provided. */
            ecs_check(out != NULL, ECS_INVALID_PARAMETER, NULL);
            *bptr++ = buff[0];
            if ((ch = buff[1])) {
                *bptr = ch;
                bptr++;
            }
        }
    }

    if (bptr) {
        while (written < n) {
            *bptr = '\0';
            bptr++;
            written++;
        }
    }
    return written;
error:
    return 0;
}

char* ecs_astresc(
    char delimiter, 
    const char *in)
{
    if (!in) {
        return NULL;
    }

    ecs_size_t len = ecs_stresc(NULL, 0, delimiter, in);
    char *out = ecs_os_malloc_n(char, len + 1);
    ecs_stresc(out, len, delimiter, in);
    out[len] = '\0';
    return out;
}

#endif



#ifdef FLECS_EXPR

const char *ecs_parse_expr_token(
    const char *name,
    const char *expr,
    const char *ptr,
    char *token)
{
    const char *start = ptr;
    char *token_ptr = token;

    while ((ptr = ecs_parse_token(name, expr, ptr, token_ptr))) {
        if (ptr[0] == '|') {
            token_ptr = &token_ptr[ptr - start];
            token_ptr[0] = '|';
            token_ptr[1] = '\0';
            token_ptr ++;
            ptr ++;
            start = ptr;
        } else {
            break;
        }
    }

    return ptr;
}

const char* ecs_parse_expr(
    const ecs_world_t *world,
    const char *ptr,
    ecs_entity_t type,
    void *data_out,
    const ecs_parse_expr_desc_t *desc)
{
    ecs_assert(ptr != NULL, ECS_INTERNAL_ERROR, NULL);
    char token[ECS_MAX_TOKEN_SIZE];
    int depth = 0;

    const char *name = NULL;
    const char *expr = NULL;

    ptr = ecs_parse_fluff(ptr, NULL);

    ecs_meta_cursor_t cur = ecs_meta_cursor(world, type, data_out);
    if (cur.valid == false) {
        return NULL;
    }

    if (desc) {
        name = desc->name;
        expr = desc->expr;
        cur.lookup_action = desc->lookup_action;
        cur.lookup_ctx = desc->lookup_ctx;
    }

    while ((ptr = ecs_parse_expr_token(name, expr, ptr, token))) {

        if (!ecs_os_strcmp(token, "{")) {
            ecs_entity_t scope_type = ecs_meta_get_type(&cur);
            depth ++;
            if (ecs_meta_push(&cur) != 0) {
                goto error;
            }

            if (ecs_meta_is_collection(&cur)) {
                char *path = ecs_get_fullpath(world, scope_type);
                ecs_parser_error(name, expr, ptr - expr, 
                    "expected '[' for collection type '%s'", path);
                ecs_os_free(path);
                return NULL;
            }
        }

        else if (!ecs_os_strcmp(token, "}")) {
            depth --;

            if (ecs_meta_is_collection(&cur)) {
                ecs_parser_error(name, expr, ptr - expr, "expected ']'");
                return NULL;
            }

            if (ecs_meta_pop(&cur) != 0) {
                goto error;
            }
        }

        else if (!ecs_os_strcmp(token, "[")) {
            depth ++;
            if (ecs_meta_push(&cur) != 0) {
                goto error;
            }

            if (!ecs_meta_is_collection(&cur)) {
                ecs_parser_error(name, expr, ptr - expr, "expected '{'");
                return NULL;
            }
        }

        else if (!ecs_os_strcmp(token, "]")) {
            depth --;

            if (!ecs_meta_is_collection(&cur)) {
                ecs_parser_error(name, expr, ptr - expr, "expected '}'");
                return NULL;
            }

            if (ecs_meta_pop(&cur) != 0) {
                goto error;
            }
        }

        else if (!ecs_os_strcmp(token, ",")) {
            if (ecs_meta_next(&cur) != 0) {
                goto error;
            }
        }

        else if (!ecs_os_strcmp(token, "null")) {
            if (ecs_meta_set_null(&cur) != 0) {
                goto error;
            }
        }

        else if (token[0] == '\"') {
            if (ecs_meta_set_string_literal(&cur, token) != 0) {
                goto error;
            }
        }

        else {
            ptr = ecs_parse_fluff(ptr, NULL);

            if (ptr[0] == ':') {
                /* Member assignment */
                ptr ++;
                if (ecs_meta_member(&cur, token) != 0) {
                    goto error;
                }
            } else {
                if (ecs_meta_set_string(&cur, token) != 0) {
                    goto error;
                }
            }
        }

        if (!depth) {
            break;
        }

        ptr = ecs_parse_fluff(ptr, NULL);
    }

    return ptr;
error:
    return NULL;
}

#endif



#ifdef FLECS_SYSTEM
#endif

#ifdef FLECS_PIPELINE
#endif

#ifdef FLECS_STATS

#include <stdio.h>

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

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

#define ECS_METRIC_FIRST(stats)\
    ECS_CAST(ecs_metric_t*, ECS_OFFSET(&stats->first_, ECS_SIZEOF(int32_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,
    float value)
{
    m->gauge.avg[t] = value;
    m->gauge.min[t] = value;
    m->gauge.max[t] = value;
}

static
float flecs_counter_record(
    ecs_metric_t *m,
    int32_t t,
    float value)
{
    int32_t tp = t_prev(t);
    float prev = m->counter.value[tp];
    m->counter.value[t] = value;
    flecs_gauge_record(m, t, value - prev);
    return value - prev;
}

static
void flecs_metric_print(
    const char *name,
    float 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);

    float delta_world_time = ECS_COUNTER_RECORD(&s->world_time_total_raw, t, world->info.world_time_total_raw);
    ECS_COUNTER_RECORD(&s->world_time_total, t, world->info.world_time_total);
    ECS_COUNTER_RECORD(&s->frame_time_total, t, world->info.frame_time_total);
    ECS_COUNTER_RECORD(&s->system_time_total, t, world->info.system_time_total);
    ECS_COUNTER_RECORD(&s->merge_time_total, t, world->info.merge_time_total);

    float delta_frame_count = ECS_COUNTER_RECORD(&s->frame_count_total, t, world->info.frame_count_total);
    ECS_COUNTER_RECORD(&s->merge_count_total, t, world->info.merge_count_total);
    ECS_COUNTER_RECORD(&s->pipeline_build_count_total, t, world->info.pipeline_build_count_total);
    ECS_COUNTER_RECORD(&s->systems_ran_frame, t, world->info.systems_ran_frame);

    if (delta_world_time != 0.0f && delta_frame_count != 0.0f) {
        ECS_GAUGE_RECORD(
            &s->fps, t, 1.0f / (delta_world_time / (float)delta_frame_count));
    } else {
        ECS_GAUGE_RECORD(&s->fps, t, 0);
    }

    ECS_GAUGE_RECORD(&s->delta_time, t, delta_world_time);

    ECS_GAUGE_RECORD(&s->entity_count, t, flecs_sparse_count(ecs_eis(world)));
    ECS_GAUGE_RECORD(&s->entity_not_alive_count, t, 
        flecs_sparse_not_alive_count(ecs_eis(world)));

    ECS_GAUGE_RECORD(&s->id_count, t, world->info.id_count);
    ECS_GAUGE_RECORD(&s->tag_id_count, t, world->info.tag_id_count);
    ECS_GAUGE_RECORD(&s->component_id_count, t, world->info.component_id_count);
    ECS_GAUGE_RECORD(&s->pair_id_count, t, world->info.pair_id_count);
    ECS_GAUGE_RECORD(&s->wildcard_id_count, t, world->info.wildcard_id_count);
    ECS_GAUGE_RECORD(&s->component_count, t, ecs_sparse_count(world->type_info));

    ECS_GAUGE_RECORD(&s->query_count, t, ecs_count_id(world, EcsQuery));
    ECS_GAUGE_RECORD(&s->observer_count, t, ecs_count_id(world, EcsObserver));
    ECS_GAUGE_RECORD(&s->system_count, t, ecs_count_id(world, EcsSystem));

    ECS_COUNTER_RECORD(&s->id_create_count, t, world->info.id_create_total);
    ECS_COUNTER_RECORD(&s->id_delete_count, t, world->info.id_delete_total);
    ECS_COUNTER_RECORD(&s->table_create_count, t, world->info.table_create_total);
    ECS_COUNTER_RECORD(&s->table_delete_count, t, world->info.table_delete_total);

    ECS_COUNTER_RECORD(&s->new_count, t, world->info.new_count);
    ECS_COUNTER_RECORD(&s->bulk_new_count, t, world->info.bulk_new_count);
    ECS_COUNTER_RECORD(&s->delete_count, t, world->info.delete_count);
    ECS_COUNTER_RECORD(&s->clear_count, t, world->info.clear_count);
    ECS_COUNTER_RECORD(&s->add_count, t, world->info.add_count);
    ECS_COUNTER_RECORD(&s->remove_count, t, world->info.remove_count);
    ECS_COUNTER_RECORD(&s->set_count, t, world->info.set_count);
    ECS_COUNTER_RECORD(&s->discard_count, t, world->info.discard_count);

    ECS_GAUGE_RECORD(&s->table_count, t, world->info.table_count);
    ECS_GAUGE_RECORD(&s->empty_table_count, t, world->info.empty_table_count);
    ECS_GAUGE_RECORD(&s->tag_table_count, t, world->info.tag_table_count);
    ECS_GAUGE_RECORD(&s->trivial_table_count, t, world->info.trivial_table_count);
    ECS_GAUGE_RECORD(&s->table_storage_count, t, world->info.table_storage_count);
    ECS_GAUGE_RECORD(&s->table_record_count, t, world->info.table_record_count);

    ECS_COUNTER_RECORD(&s->alloc_count, t, ecs_os_api_malloc_count + 
        ecs_os_api_calloc_count);
    ECS_COUNTER_RECORD(&s->realloc_count, t, ecs_os_api_realloc_count);
    ECS_COUNTER_RECORD(&s->free_count, t, ecs_os_api_free_count);

    int64_t outstanding_allocs = ecs_os_api_malloc_count + 
        ecs_os_api_calloc_count - ecs_os_api_free_count;
    ECS_GAUGE_RECORD(&s->outstanding_alloc_count, t, outstanding_allocs);

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);
    ECS_GAUGE_RECORD(&s->active, t, !ecs_has_id(world, system, EcsEmpty));
    ECS_GAUGE_RECORD(&s->enabled, t, !ecs_has_id(world, system, EcsDisabled));

    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 *pq = ecs_get(world, pipeline, EcsPipeline);
    if (!pq) {
        return false;
    }

    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_vector_t *ops = pq->ops;
    ecs_pipeline_op_t *op = ecs_vector_first(ops, ecs_pipeline_op_t);
    ecs_pipeline_op_t *op_last = ecs_vector_last(ops, ecs_pipeline_op_t);
    int32_t pip_count = active_sys_count + ecs_vector_count(ops);

    if (!sys_count) {
        return false;
    }

    if (ecs_map_is_initialized(&s->system_stats) && !sys_count) {
        ecs_map_fini(&s->system_stats);
    }
    if (!ecs_map_is_initialized(&s->system_stats) && sys_count) {
        ecs_map_init(&s->system_stats, ecs_system_stats_t, sys_count);
    }

    /* Make sure vector is large enough to store all systems & sync points */
    ecs_entity_t *systems = NULL;
    if (pip_count) {
        ecs_vector_set_count(&s->systems, ecs_entity_t, pip_count);
        systems = ecs_vector_first(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_vector_free(s->systems);
        s->systems = NULL;
    }

    /* 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)) {
        int i;
        for (i = 0; i < it.count; i ++) {
            ecs_system_stats_t *sys_stats = ecs_map_ensure(
                    &s->system_stats, ecs_system_stats_t, it.entities[i]);
            sys_stats->query.t = s->t;
            ecs_system_stats_get(world, it.entities[i], sys_stats);
        }
    }

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

    return true;
error:
    return false;
}

void ecs_pipeline_stats_fini(
    ecs_pipeline_stats_t *stats)
{
    ecs_map_fini(&stats->system_stats);
    ecs_vector_free(stats->systems);
}

void ecs_pipeline_stats_reduce(
    ecs_pipeline_stats_t *dst,
    const ecs_pipeline_stats_t *src)
{
    int32_t system_count = ecs_vector_count(src->systems);
    ecs_vector_set_count(&dst->systems, ecs_entity_t, system_count);
    ecs_entity_t *dst_systems = ecs_vector_first(dst->systems, ecs_entity_t);
    ecs_entity_t *src_systems = ecs_vector_first(src->systems, ecs_entity_t);
    ecs_os_memcpy_n(dst_systems, src_systems, ecs_entity_t, system_count);

    if (!ecs_map_is_initialized(&dst->system_stats)) {
        ecs_map_init(&dst->system_stats, ecs_system_stats_t,
            ecs_map_count(&src->system_stats));
    }

    ecs_map_iter_t it = ecs_map_iter(&src->system_stats);
    ecs_system_stats_t *sys_src, *sys_dst;
    ecs_entity_t key;
    while ((sys_src = ecs_map_next(&it, ecs_system_stats_t, &key))) {
        sys_dst = ecs_map_ensure(&dst->system_stats, ecs_system_stats_t, key);
        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)
{
    if (!ecs_map_is_initialized(&dst->system_stats)) {
        ecs_map_init(&dst->system_stats, ecs_system_stats_t,
            ecs_map_count(&src->system_stats));
    }

    ecs_map_iter_t it = ecs_map_iter(&src->system_stats);
    ecs_system_stats_t *sys_src, *sys_dst;
    ecs_entity_t key;
    while ((sys_src = ecs_map_next(&it, ecs_system_stats_t, &key))) {
        sys_dst = ecs_map_ensure(&dst->system_stats, ecs_system_stats_t, key);
        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)
{
    ecs_map_iter_t it = ecs_map_iter(&stats->system_stats);
    ecs_system_stats_t *sys;
    ecs_entity_t key;
    while ((sys = ecs_map_next(&it, ecs_system_stats_t, &key))) {
        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)
{
    if (!ecs_map_is_initialized(&dst->system_stats)) {
        ecs_map_init(&dst->system_stats, ecs_system_stats_t,
            ecs_map_count(&src->system_stats));
    }

    ecs_map_iter_t it = ecs_map_iter(&src->system_stats);
    ecs_system_stats_t *sys_src, *sys_dst;
    ecs_entity_t key;
    while ((sys_src = ecs_map_next(&it, ecs_system_stats_t, &key))) {
        sys_dst = ecs_map_ensure(&dst->system_stats, ecs_system_stats_t, key);
        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_count_total);
    ecs_trace("-------------------------------------");
    flecs_counter_print("pipeline rebuilds", t, &s->pipeline_build_count_total);
    flecs_counter_print("systems invocations", t, &s->systems_ran_frame);
    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->fps);
    flecs_counter_print("frame time", t, &s->frame_time_total);
    flecs_counter_print("system time", t, &s->system_time_total);
    flecs_counter_print("merge time", t, &s->merge_time_total);
    flecs_counter_print("simulation time elapsed", t, &s->world_time_total);
    ecs_trace("");
    flecs_gauge_print("id count", t, &s->id_count);
    flecs_gauge_print("tag id count", t, &s->tag_id_count);
    flecs_gauge_print("component id count", t, &s->component_id_count);
    flecs_gauge_print("pair id count", t, &s->pair_id_count);
    flecs_gauge_print("wildcard id count", t, &s->wildcard_id_count);
    flecs_gauge_print("component count", t, &s->component_count);
    ecs_trace("");
    flecs_gauge_print("alive entity count", t, &s->entity_count);
    flecs_gauge_print("not alive entity count", t, &s->entity_not_alive_count);
    ecs_trace("");
    flecs_gauge_print("query count", t, &s->query_count);
    flecs_gauge_print("observer count", t, &s->observer_count);
    flecs_gauge_print("system count", t, &s->system_count);
    ecs_trace("");
    flecs_gauge_print("table count", t, &s->table_count);
    flecs_gauge_print("empty table count", t, &s->empty_table_count);
    flecs_gauge_print("tag table count", t, &s->tag_table_count);
    flecs_gauge_print("trivial table count", t, &s->trivial_table_count);
    flecs_gauge_print("table storage count", t, &s->table_storage_count);
    flecs_gauge_print("table cache record count", t, &s->table_record_count);
    ecs_trace("");
    flecs_counter_print("table create count", t, &s->table_create_count);
    flecs_counter_print("table delete count", t, &s->table_delete_count);
    flecs_counter_print("id create count", t, &s->id_create_count);
    flecs_counter_print("id delete count", t, &s->id_delete_count);
    ecs_trace("");
    flecs_counter_print("deferred new operations", t, &s->new_count);
    flecs_counter_print("deferred bulk_new operations", t, &s->bulk_new_count);
    flecs_counter_print("deferred delete operations", t, &s->delete_count);
    flecs_counter_print("deferred clear operations", t, &s->clear_count);
    flecs_counter_print("deferred add operations", t, &s->add_count);
    flecs_counter_print("deferred remove operations", t, &s->remove_count);
    flecs_counter_print("deferred set operations", t, &s->set_count);
    flecs_counter_print("discarded operations", t, &s->discard_count);
    ecs_trace("");
    
error:
    return;
}

#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(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);

void FlecsUnitsImport(
    ecs_world_t *world)
{
    ECS_MODULE(world, FlecsUnits);

    ecs_set_name_prefix(world, "Ecs");

    EcsUnitPrefixes = ecs_entity_init(world, &(ecs_entity_desc_t){
        .name = "prefixes",
        .add = { EcsModule }
    });

    /* Initialize unit prefixes */

    ecs_entity_t prev_scope = ecs_set_scope(world, EcsUnitPrefixes);

    EcsYocto = ecs_unit_prefix_init(world, &(ecs_unit_prefix_desc_t){
        .entity = ecs_entity(world, { .name = "Yocto" }),
        .symbol = "y",
        .translation = { .factor = 10, .power = -24 }
    });
    EcsZepto = ecs_unit_prefix_init(world, &(ecs_unit_prefix_desc_t){
        .entity = ecs_entity(world, { .name = "Zepto" }),
        .symbol = "z",
        .translation = { .factor = 10, .power = -21 }
    });
    EcsAtto = ecs_unit_prefix_init(world, &(ecs_unit_prefix_desc_t){
        .entity = ecs_entity(world, { .name = "Atto" }),
        .symbol = "a",
        .translation = { .factor = 10, .power = -18 }
    });
    EcsFemto = ecs_unit_prefix_init(world, &(ecs_unit_prefix_desc_t){
        .entity = ecs_entity(world, { .name = "Femto" }),
        .symbol = "a",
        .translation = { .factor = 10, .power = -15 }
    });
    EcsPico = ecs_unit_prefix_init(world, &(ecs_unit_prefix_desc_t){
        .entity = ecs_entity(world, { .name = "Pico" }),
        .symbol = "p",
        .translation = { .factor = 10, .power = -12 }
    });
    EcsNano = ecs_unit_prefix_init(world, &(ecs_unit_prefix_desc_t){
        .entity = ecs_entity(world, { .name = "Nano" }),
        .symbol = "n",
        .translation = { .factor = 10, .power = -9 }
    });
    EcsMicro = ecs_unit_prefix_init(world, &(ecs_unit_prefix_desc_t){
        .entity = ecs_entity(world, { .name = "Micro" }),
        .symbol = "μ",
        .translation = { .factor = 10, .power = -6 }
    });
    EcsMilli = ecs_unit_prefix_init(world, &(ecs_unit_prefix_desc_t){
        .entity = ecs_entity(world, { .name = "Milli" }),
        .symbol = "m",
        .translation = { .factor = 10, .power = -3 }
    });
    EcsCenti = ecs_unit_prefix_init(world, &(ecs_unit_prefix_desc_t){
        .entity = ecs_entity(world, { .name = "Centi" }),
        .symbol = "c",
        .translation = { .factor = 10, .power = -2 }
    });
    EcsDeci = ecs_unit_prefix_init(world, &(ecs_unit_prefix_desc_t){
        .entity = ecs_entity(world, { .name = "Deci" }),
        .symbol = "d",
        .translation = { .factor = 10, .power = -1 }
    });
    EcsDeca = ecs_unit_prefix_init(world, &(ecs_unit_prefix_desc_t){
        .entity = ecs_entity(world, { .name = "Deca" }),
        .symbol = "da",
        .translation = { .factor = 10, .power = 1 }
    });
    EcsHecto = ecs_unit_prefix_init(world, &(ecs_unit_prefix_desc_t){
        .entity = ecs_entity(world, { .name = "Hecto" }),
        .symbol = "h",
        .translation = { .factor = 10, .power = 2 }
    });
    EcsKilo = ecs_unit_prefix_init(world, &(ecs_unit_prefix_desc_t){
        .entity = ecs_entity(world, { .name = "Kilo" }),
        .symbol = "k",
        .translation = { .factor = 10, .power = 3 }
    });
    EcsMega = ecs_unit_prefix_init(world, &(ecs_unit_prefix_desc_t){
        .entity = ecs_entity(world, { .name = "Mega" }),
        .symbol = "M",
        .translation = { .factor = 10, .power = 6 }
    });
    EcsGiga = ecs_unit_prefix_init(world, &(ecs_unit_prefix_desc_t){
        .entity = ecs_entity(world, { .name = "Giga" }),
        .symbol = "G",
        .translation = { .factor = 10, .power = 9 }
    });
    EcsTera = ecs_unit_prefix_init(world, &(ecs_unit_prefix_desc_t){
        .entity = ecs_entity(world, { .name = "Tera" }),
        .symbol = "T",
        .translation = { .factor = 10, .power = 12 }
    });
    EcsPeta = ecs_unit_prefix_init(world, &(ecs_unit_prefix_desc_t){
        .entity = ecs_entity(world, { .name = "Peta" }),
        .symbol = "P",
        .translation = { .factor = 10, .power = 15 }
    });
    EcsExa = ecs_unit_prefix_init(world, &(ecs_unit_prefix_desc_t){
        .entity = ecs_entity(world, { .name = "Exa" }),
        .symbol = "E",
        .translation = { .factor = 10, .power = 18 }
    });
    EcsZetta = ecs_unit_prefix_init(world, &(ecs_unit_prefix_desc_t){
        .entity = ecs_entity(world, { .name = "Zetta" }),
        .symbol = "Z",
        .translation = { .factor = 10, .power = 21 }
    });
    EcsYotta = ecs_unit_prefix_init(world, &(ecs_unit_prefix_desc_t){
        .entity = ecs_entity(world, { .name = "Yotta" }),
        .symbol = "Y",
        .translation = { .factor = 10, .power = 24 }
    });

    EcsKibi = ecs_unit_prefix_init(world, &(ecs_unit_prefix_desc_t){
        .entity = ecs_entity(world, { .name = "Kibi" }),
        .symbol = "Ki",
        .translation = { .factor = 1024, .power = 1 }
    });
    EcsMebi = ecs_unit_prefix_init(world, &(ecs_unit_prefix_desc_t){
        .entity = ecs_entity(world, { .name = "Mebi" }),
        .symbol = "Mi",
        .translation = { .factor = 1024, .power = 2 }
    });
    EcsGibi = ecs_unit_prefix_init(world, &(ecs_unit_prefix_desc_t){
        .entity = ecs_entity(world, { .name = "Gibi" }),
        .symbol = "Gi",
        .translation = { .factor = 1024, .power = 3 }
    });
    EcsTebi = ecs_unit_prefix_init(world, &(ecs_unit_prefix_desc_t){
        .entity = ecs_entity(world, { .name = "Tebi" }),
        .symbol = "Ti",
        .translation = { .factor = 1024, .power = 4 }
    });
    EcsPebi = ecs_unit_prefix_init(world, &(ecs_unit_prefix_desc_t){
        .entity = ecs_entity(world, { .name = "Pebi" }),
        .symbol = "Pi",
        .translation = { .factor = 1024, .power = 5 }
    });
    EcsExbi = ecs_unit_prefix_init(world, &(ecs_unit_prefix_desc_t){
        .entity = ecs_entity(world, { .name = "Exbi" }),
        .symbol = "Ei",
        .translation = { .factor = 1024, .power = 6 }
    });
    EcsZebi = ecs_unit_prefix_init(world, &(ecs_unit_prefix_desc_t){
        .entity = ecs_entity(world, { .name = "Zebi" }),
        .symbol = "Zi",
        .translation = { .factor = 1024, .power = 7 }
    });
    EcsYobi = ecs_unit_prefix_init(world, &(ecs_unit_prefix_desc_t){
        .entity = ecs_entity(world, { .name = "Yobi" }),
        .symbol = "Yi",
        .translation = { .factor = 1024, .power = 8 }
    });

    ecs_set_scope(world, prev_scope);

    /* Duration units */

    EcsDuration = ecs_quantity_init(world, &(ecs_entity_desc_t){ 
        .name = "Duration" });
    prev_scope = ecs_set_scope(world, EcsDuration);

        EcsSeconds = ecs_unit_init(world, &(ecs_unit_desc_t){
            .entity = ecs_entity(world, { .name = "Seconds" }),
            .quantity = EcsDuration,
            .symbol = "s" });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsSeconds,
            .kind = EcsF32
        });
            EcsPicoSeconds = ecs_unit_init(world, &(ecs_unit_desc_t){
                .entity = ecs_entity(world, { .name = "PicoSeconds" }),
                .quantity = EcsDuration,
                .base = EcsSeconds,
                .prefix = EcsPico });
            ecs_primitive_init(world, &(ecs_primitive_desc_t){
                .entity = EcsPicoSeconds,
                .kind = EcsF32
            });


            EcsNanoSeconds = ecs_unit_init(world, &(ecs_unit_desc_t){
                .entity = ecs_entity(world, { .name = "NanoSeconds" }),
                .quantity = EcsDuration,
                .base = EcsSeconds,
                .prefix = EcsNano });
            ecs_primitive_init(world, &(ecs_primitive_desc_t){
                .entity = EcsNanoSeconds,
                .kind = EcsF32
            });

            EcsMicroSeconds = ecs_unit_init(world, &(ecs_unit_desc_t){
                .entity = ecs_entity(world, { .name = "MicroSeconds" }),
                .quantity = EcsDuration,
                .base = EcsSeconds,
                .prefix = EcsMicro });
            ecs_primitive_init(world, &(ecs_primitive_desc_t){
                .entity = EcsMicroSeconds,
                .kind = EcsF32
            });

            EcsMilliSeconds = ecs_unit_init(world, &(ecs_unit_desc_t){
                .entity = ecs_entity(world, { .name = "MilliSeconds" }),
                .quantity = EcsDuration,
                .base = EcsSeconds,
                .prefix = EcsMilli });
            ecs_primitive_init(world, &(ecs_primitive_desc_t){
                .entity = EcsMilliSeconds,
                .kind = EcsF32
            });

        EcsMinutes = ecs_unit_init(world, &(ecs_unit_desc_t){
            .entity = ecs_entity(world, { .name = "Minutes" }),
            .quantity = EcsDuration,
            .base = EcsSeconds,
            .symbol = "min",
            .translation = { .factor = 60, .power = 1 } });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsMinutes,
            .kind = EcsU32
        });

        EcsHours = ecs_unit_init(world, &(ecs_unit_desc_t){
            .entity = ecs_entity(world, { .name = "Hours" }),
            .quantity = EcsDuration,
            .base = EcsMinutes,
            .symbol = "h",
            .translation = { .factor = 60, .power = 1 } });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsHours,
            .kind = EcsU32
        });

        EcsDays = ecs_unit_init(world, &(ecs_unit_desc_t){
            .entity = ecs_entity(world, { .name = "Days" }),
            .quantity = EcsDuration,
            .base = EcsHours,
            .symbol = "d",
            .translation = { .factor = 24, .power = 1 } });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsDays,
            .kind = EcsU32
        });
    ecs_set_scope(world, prev_scope);

    /* Time units */

    EcsTime = ecs_quantity_init(world, &(ecs_entity_desc_t){ 
        .name = "Time" });
    prev_scope = ecs_set_scope(world, EcsTime);

        EcsDate = ecs_unit_init(world, &(ecs_unit_desc_t){ 
            .entity = ecs_entity(world, { .name = "Date" }),
            .quantity = EcsTime });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsDate,
            .kind = EcsU32
        });
    ecs_set_scope(world, prev_scope);

    /* Mass units */

    EcsMass = ecs_quantity_init(world, &(ecs_entity_desc_t){ 
        .name = "Mass" });
    prev_scope = ecs_set_scope(world, EcsMass);
        EcsGrams = ecs_unit_init(world, &(ecs_unit_desc_t){ 
            .entity = ecs_entity(world, { .name = "Grams" }),
            .quantity = EcsMass,
            .symbol = "g" });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsGrams,
            .kind = EcsF32
        });
        EcsKiloGrams = ecs_unit_init(world, &(ecs_unit_desc_t){ 
            .entity = ecs_entity(world, { .name = "KiloGrams" }),
            .quantity = EcsMass,
            .prefix = EcsKilo,
            .base = EcsGrams });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsKiloGrams,
            .kind = EcsF32
        });
    ecs_set_scope(world, prev_scope);

    /* Electric current units */

    EcsElectricCurrent = ecs_quantity_init(world, &(ecs_entity_desc_t){ 
        .name = "ElectricCurrent" });
    prev_scope = ecs_set_scope(world, EcsElectricCurrent);
        EcsAmpere = ecs_unit_init(world, &(ecs_unit_desc_t){ 
            .entity = ecs_entity(world, { .name = "Ampere" }),
            .quantity = EcsElectricCurrent,
            .symbol = "A" });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsAmpere,
            .kind = EcsF32
        });
    ecs_set_scope(world, prev_scope);

    /* Amount of substance units */

    EcsAmount = ecs_quantity_init(world, &(ecs_entity_desc_t){ 
        .name = "Amount" });
    prev_scope = ecs_set_scope(world, EcsAmount);
        EcsMole = ecs_unit_init(world, &(ecs_unit_desc_t){ 
            .entity = ecs_entity(world, { .name = "Mole" }),
            .quantity = EcsAmount,
            .symbol = "mol" });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsMole,
            .kind = EcsF32
        });
    ecs_set_scope(world, prev_scope);

    /* Luminous intensity units */

    EcsLuminousIntensity = ecs_quantity_init(world, &(ecs_entity_desc_t){ 
        .name = "LuminousIntensity" });
    prev_scope = ecs_set_scope(world, EcsLuminousIntensity);
        EcsCandela = ecs_unit_init(world, &(ecs_unit_desc_t){ 
            .entity = ecs_entity(world, { .name = "Candela" }),
            .quantity = EcsLuminousIntensity,
            .symbol = "cd" });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsCandela,
            .kind = EcsF32
        });
    ecs_set_scope(world, prev_scope);

    /* Force units */

    EcsForce = ecs_quantity_init(world, &(ecs_entity_desc_t){ 
        .name = "Force" });
    prev_scope = ecs_set_scope(world, EcsForce);
        EcsNewton = ecs_unit_init(world, &(ecs_unit_desc_t){ 
            .entity = ecs_entity(world, { .name = "Newton" }),
            .quantity = EcsForce,
            .symbol = "N" });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsNewton,
            .kind = EcsF32
        });
    ecs_set_scope(world, prev_scope);

    /* Length units */

    EcsLength = ecs_quantity_init(world, &(ecs_entity_desc_t){ 
        .name = "Length" });
    prev_scope = ecs_set_scope(world, EcsLength);
        EcsMeters = ecs_unit_init(world, &(ecs_unit_desc_t){ 
            .entity = ecs_entity(world, { .name = "Meters" }),
            .quantity = EcsLength,
            .symbol = "m" });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsMeters,
            .kind = EcsF32
        });

            EcsPicoMeters = ecs_unit_init(world, &(ecs_unit_desc_t){ 
                .entity = ecs_entity(world, { .name = "PicoMeters" }),
                .quantity = EcsLength,
                .base = EcsMeters,
                .prefix = EcsPico });
            ecs_primitive_init(world, &(ecs_primitive_desc_t){
                .entity = EcsPicoMeters,
                .kind = EcsF32
            });

            EcsNanoMeters = ecs_unit_init(world, &(ecs_unit_desc_t){ 
                .entity = ecs_entity(world, { .name = "NanoMeters" }),
                .quantity = EcsLength,
                .base = EcsMeters,
                .prefix = EcsNano });
            ecs_primitive_init(world, &(ecs_primitive_desc_t){
                .entity = EcsNanoMeters,
                .kind = EcsF32
            });

            EcsMicroMeters = ecs_unit_init(world, &(ecs_unit_desc_t){ 
                .entity = ecs_entity(world, { .name = "MicroMeters" }),
                .quantity = EcsLength,
                .base = EcsMeters,
                .prefix = EcsMicro });
            ecs_primitive_init(world, &(ecs_primitive_desc_t){
                .entity = EcsMicroMeters,
                .kind = EcsF32
            });

            EcsMilliMeters = ecs_unit_init(world, &(ecs_unit_desc_t){ 
                .entity = ecs_entity(world, { .name = "MilliMeters" }),
                .quantity = EcsLength,
                .base = EcsMeters,
                .prefix = EcsMilli });
            ecs_primitive_init(world, &(ecs_primitive_desc_t){
                .entity = EcsMilliMeters,
                .kind = EcsF32
            });

            EcsCentiMeters = ecs_unit_init(world, &(ecs_unit_desc_t){ 
                .entity = ecs_entity(world, { .name = "CentiMeters" }),
                .quantity = EcsLength,
                .base = EcsMeters,
                .prefix = EcsCenti });
            ecs_primitive_init(world, &(ecs_primitive_desc_t){
                .entity = EcsCentiMeters,
                .kind = EcsF32
            });

            EcsKiloMeters = ecs_unit_init(world, &(ecs_unit_desc_t){ 
                .entity = ecs_entity(world, { .name = "KiloMeters" }),
                .quantity = EcsLength,
                .base = EcsMeters,
                .prefix = EcsKilo });
            ecs_primitive_init(world, &(ecs_primitive_desc_t){
                .entity = EcsKiloMeters,
                .kind = EcsF32
            });
            
        EcsMiles = ecs_unit_init(world, &(ecs_unit_desc_t){ 
            .entity = ecs_entity(world, { .name = "Miles" }),
            .quantity = EcsLength,
            .symbol = "mi"
        });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsMiles,
            .kind = EcsF32
        });
    ecs_set_scope(world, prev_scope);

    /* Pressure units */

    EcsPressure = ecs_quantity_init(world, &(ecs_entity_desc_t){ 
        .name = "Pressure" });
    prev_scope = ecs_set_scope(world, EcsPressure);
        EcsPascal = ecs_unit_init(world, &(ecs_unit_desc_t){ 
            .entity = ecs_entity(world, { .name = "Pascal" }),
            .quantity = EcsPressure,
            .symbol = "Pa" });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsPascal,
            .kind = EcsF32
        });
        EcsBar = ecs_unit_init(world, &(ecs_unit_desc_t){ 
            .entity = ecs_entity(world, { .name = "Bar" }),
            .quantity = EcsPressure,
            .symbol = "bar" });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsBar,
            .kind = EcsF32
        });
    ecs_set_scope(world, prev_scope);

    /* Speed units */

    EcsSpeed = ecs_quantity_init(world, &(ecs_entity_desc_t){ 
        .name = "Speed" });
    prev_scope = ecs_set_scope(world, EcsSpeed);
        EcsMetersPerSecond = ecs_unit_init(world, &(ecs_unit_desc_t){ 
            .entity = ecs_entity(world, { .name = "MetersPerSecond" }),
            .quantity = EcsSpeed,
            .base = EcsMeters,
            .over = EcsSeconds });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsMetersPerSecond,
            .kind = EcsF32
        });
        EcsKiloMetersPerSecond = ecs_unit_init(world, &(ecs_unit_desc_t){ 
            .entity = ecs_entity(world, { .name = "KiloMetersPerSecond" }),
            .quantity = EcsSpeed,
            .base = EcsKiloMeters,
            .over = EcsSeconds });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsKiloMetersPerSecond,
            .kind = EcsF32
        });
        EcsKiloMetersPerHour = ecs_unit_init(world, &(ecs_unit_desc_t){ 
            .entity = ecs_entity(world, { .name = "KiloMetersPerHour" }),
            .quantity = EcsSpeed,
            .base = EcsKiloMeters,
            .over = EcsHours });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsKiloMetersPerHour,
            .kind = EcsF32
        });
        EcsMilesPerHour = ecs_unit_init(world, &(ecs_unit_desc_t){ 
            .entity = ecs_entity(world, { .name = "MilesPerHour" }),
            .quantity = EcsSpeed,
            .base = EcsMiles,
            .over = EcsHours });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsMilesPerHour,
            .kind = EcsF32
        });
    ecs_set_scope(world, prev_scope);
    
    /* Acceleration */

    EcsAcceleration = ecs_unit_init(world, &(ecs_unit_desc_t){ 
        .entity = ecs_entity(world, { .name = "Acceleration" }),
        .base = EcsMetersPerSecond,
        .over = EcsSeconds });
    ecs_quantity_init(world, &(ecs_entity_desc_t){
        .id = EcsAcceleration
    });
    ecs_primitive_init(world, &(ecs_primitive_desc_t){
        .entity = EcsAcceleration,
        .kind = EcsF32
    });

    /* Temperature units */

    EcsTemperature = ecs_quantity_init(world, &(ecs_entity_desc_t){ 
        .name = "Temperature" });
    prev_scope = ecs_set_scope(world, EcsTemperature);
        EcsKelvin = ecs_unit_init(world, &(ecs_unit_desc_t){ 
            .entity = ecs_entity(world, { .name = "Kelvin" }),
            .quantity = EcsTemperature,
            .symbol = "K" });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsKelvin,
            .kind = EcsF32
        });
        EcsCelsius = ecs_unit_init(world, &(ecs_unit_desc_t){ 
            .entity = ecs_entity(world, { .name = "Celsius" }),
            .quantity = EcsTemperature,
            .symbol = "°C" });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsCelsius,
            .kind = EcsF32
        });
        EcsFahrenheit = ecs_unit_init(world, &(ecs_unit_desc_t){ 
            .entity = ecs_entity(world, { .name = "Fahrenheit" }),
            .quantity = EcsTemperature,
            .symbol = "F" });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsFahrenheit,
            .kind = EcsF32
        });
    ecs_set_scope(world, prev_scope);

    /* Data units */

    EcsData = ecs_quantity_init(world, &(ecs_entity_desc_t){ 
        .name = "Data" });
    prev_scope = ecs_set_scope(world, EcsData);

        EcsBits = ecs_unit_init(world, &(ecs_unit_desc_t){ 
            .entity = ecs_entity(world, { .name = "Bits" }),
            .quantity = EcsData,
            .symbol = "bit" });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsBits,
            .kind = EcsU64
        });

            EcsKiloBits = ecs_unit_init(world, &(ecs_unit_desc_t){ 
                .entity = ecs_entity(world, { .name = "KiloBits" }),
                .quantity = EcsData,
                .base = EcsBits,
                .prefix = EcsKilo });
            ecs_primitive_init(world, &(ecs_primitive_desc_t){
                .entity = EcsKiloBits,
                .kind = EcsU64
            });

            EcsMegaBits = ecs_unit_init(world, &(ecs_unit_desc_t){ 
                .entity = ecs_entity(world, { .name = "MegaBits" }),
                .quantity = EcsData,
                .base = EcsBits,
                .prefix = EcsMega });
            ecs_primitive_init(world, &(ecs_primitive_desc_t){
                .entity = EcsMegaBits,
                .kind = EcsU64
            });

            EcsGigaBits = ecs_unit_init(world, &(ecs_unit_desc_t){ 
                .entity = ecs_entity(world, { .name = "GigaBits" }),
                .quantity = EcsData,
                .base = EcsBits,
                .prefix = EcsGiga });
            ecs_primitive_init(world, &(ecs_primitive_desc_t){
                .entity = EcsGigaBits,
                .kind = EcsU64
            });

        EcsBytes = ecs_unit_init(world, &(ecs_unit_desc_t){ 
            .entity = ecs_entity(world, { .name = "Bytes" }),
            .quantity = EcsData,
            .symbol = "B",
            .base = EcsBits,
            .translation = { .factor = 8, .power = 1 } });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsBytes,
            .kind = EcsU64
        });

            EcsKiloBytes = ecs_unit_init(world, &(ecs_unit_desc_t){ 
                .entity = ecs_entity(world, { .name = "KiloBytes" }),
                .quantity = EcsData,
                .base = EcsBytes,
                .prefix = EcsKilo });
            ecs_primitive_init(world, &(ecs_primitive_desc_t){
                .entity = EcsKiloBytes,
                .kind = EcsU64
            });

            EcsMegaBytes = ecs_unit_init(world, &(ecs_unit_desc_t){ 
                .entity = ecs_entity(world, { .name = "MegaBytes" }),
                .quantity = EcsData,
                .base = EcsBytes,
                .prefix = EcsMega });
            ecs_primitive_init(world, &(ecs_primitive_desc_t){
                .entity = EcsMegaBytes,
                .kind = EcsU64
            });

            EcsGigaBytes = ecs_unit_init(world, &(ecs_unit_desc_t){ 
                .entity = ecs_entity(world, { .name = "GigaBytes" }),
                .quantity = EcsData,
                .base = EcsBytes,
                .prefix = EcsGiga });
            ecs_primitive_init(world, &(ecs_primitive_desc_t){
                .entity = EcsGigaBytes,
                .kind = EcsU64
            });

            EcsKibiBytes = ecs_unit_init(world, &(ecs_unit_desc_t){ 
                .entity = ecs_entity(world, { .name = "KibiBytes" }),
                .quantity = EcsData,
                .base = EcsBytes,
                .prefix = EcsKibi });
            ecs_primitive_init(world, &(ecs_primitive_desc_t){
                .entity = EcsKibiBytes,
                .kind = EcsU64
            });

            EcsMebiBytes = ecs_unit_init(world, &(ecs_unit_desc_t){ 
                .entity = ecs_entity(world, { .name = "MebiBytes" }),
                .quantity = EcsData,
                .base = EcsBytes,
                .prefix = EcsMebi });
            ecs_primitive_init(world, &(ecs_primitive_desc_t){
                .entity = EcsMebiBytes,
                .kind = EcsU64
            });

            EcsGibiBytes = ecs_unit_init(world, &(ecs_unit_desc_t){ 
                .entity = ecs_entity(world, { .name = "GibiBytes" }),
                .quantity = EcsData,
                .base = EcsBytes,
                .prefix = EcsGibi });
            ecs_primitive_init(world, &(ecs_primitive_desc_t){
                .entity = EcsGibiBytes,
                .kind = EcsU64
            });

    ecs_set_scope(world, prev_scope);

    /* DataRate units */

    EcsDataRate = ecs_quantity_init(world, &(ecs_entity_desc_t){ 
        .name = "DataRate" });
    prev_scope = ecs_set_scope(world, EcsDataRate);

        EcsBitsPerSecond = ecs_unit_init(world, &(ecs_unit_desc_t){ 
            .entity = ecs_entity(world, { .name = "BitsPerSecond" }),
            .quantity = EcsDataRate,
            .base = EcsBits,
            .over = EcsSeconds });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsBitsPerSecond,
            .kind = EcsU64
        });

            EcsKiloBitsPerSecond = ecs_unit_init(world, &(ecs_unit_desc_t){ 
                .entity = ecs_entity(world, { .name = "KiloBitsPerSecond" }),
                .quantity = EcsDataRate,
                .base = EcsKiloBits,
                .over = EcsSeconds
            });
            ecs_primitive_init(world, &(ecs_primitive_desc_t){
                .entity = EcsKiloBitsPerSecond,
                .kind = EcsU64
            });

            EcsMegaBitsPerSecond = ecs_unit_init(world, &(ecs_unit_desc_t){ 
                .entity = ecs_entity(world, { .name = "MegaBitsPerSecond" }),
                .quantity = EcsDataRate,
                .base = EcsMegaBits,
                .over = EcsSeconds
            });
            ecs_primitive_init(world, &(ecs_primitive_desc_t){
                .entity = EcsMegaBitsPerSecond,
                .kind = EcsU64
            });

            EcsGigaBitsPerSecond = ecs_unit_init(world, &(ecs_unit_desc_t){ 
                .entity = ecs_entity(world, { .name = "GigaBitsPerSecond" }),
                .quantity = EcsDataRate,
                .base = EcsGigaBits,
                .over = EcsSeconds
            });
            ecs_primitive_init(world, &(ecs_primitive_desc_t){
                .entity = EcsGigaBitsPerSecond,
                .kind = EcsU64
            });

        EcsBytesPerSecond = ecs_unit_init(world, &(ecs_unit_desc_t){ 
            .entity = ecs_entity(world, { .name = "BytesPerSecond" }),
            .quantity = EcsDataRate,
            .base = EcsBytes,
            .over = EcsSeconds });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsBytesPerSecond,
            .kind = EcsU64
        });

            EcsKiloBytesPerSecond = ecs_unit_init(world, &(ecs_unit_desc_t){ 
                .entity = ecs_entity(world, { .name = "KiloBytesPerSecond" }),
                .quantity = EcsDataRate,
                .base = EcsKiloBytes,
                .over = EcsSeconds
            });
            ecs_primitive_init(world, &(ecs_primitive_desc_t){
                .entity = EcsKiloBytesPerSecond,
                .kind = EcsU64
            });

            EcsMegaBytesPerSecond = ecs_unit_init(world, &(ecs_unit_desc_t){ 
                .entity = ecs_entity(world, { .name = "MegaBytesPerSecond" }),
                .quantity = EcsDataRate,
                .base = EcsMegaBytes,
                .over = EcsSeconds
            });
            ecs_primitive_init(world, &(ecs_primitive_desc_t){
                .entity = EcsMegaBytesPerSecond,
                .kind = EcsU64
            });

            EcsGigaBytesPerSecond = ecs_unit_init(world, &(ecs_unit_desc_t){ 
                .entity = ecs_entity(world, { .name = "GigaBytesPerSecond" }),
                .quantity = EcsDataRate,
                .base = EcsGigaBytes,
                .over = EcsSeconds
            });
            ecs_primitive_init(world, &(ecs_primitive_desc_t){
                .entity = EcsGigaBytesPerSecond,
                .kind = EcsU64
            });

        ecs_set_scope(world, prev_scope);

    /* Percentage */

    EcsPercentage = ecs_quantity_init(world, &(ecs_entity_desc_t){ 
        .name = "Percentage" });
    ecs_unit_init(world, &(ecs_unit_desc_t){ 
        .entity = EcsPercentage,
        .symbol = "%"
    });
    ecs_primitive_init(world, &(ecs_primitive_desc_t){
        .entity = EcsPercentage,
        .kind = EcsF32
    });

    /* Angles */

    EcsAngle = ecs_quantity_init(world, &(ecs_entity_desc_t){ 
        .name = "Angle" });
    prev_scope = ecs_set_scope(world, EcsAngle);
        EcsRadians = ecs_unit_init(world, &(ecs_unit_desc_t){ 
            .entity = ecs_entity(world, { .name = "Radians" }),
            .quantity = EcsAngle,
            .symbol = "rad" });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsRadians,
            .kind = EcsF32
        });

        EcsDegrees = ecs_unit_init(world, &(ecs_unit_desc_t){ 
            .entity = ecs_entity(world, { .name = "Degrees" }),
            .quantity = EcsAngle,
            .symbol = "°" });
        ecs_primitive_init(world, &(ecs_primitive_desc_t){
            .entity = EcsDegrees,
            .kind = EcsF32
        });
    ecs_set_scope(world, prev_scope);

    /* DeciBel */

    EcsBel = ecs_unit_init(world, &(ecs_unit_desc_t){ 
        .entity = ecs_entity(world, { .name = "Bel" }),
        .symbol = "B" });
    ecs_primitive_init(world, &(ecs_primitive_desc_t){
        .entity = EcsBel,
        .kind = EcsF32
    });
    EcsDeciBel = ecs_unit_init(world, &(ecs_unit_desc_t){ 
        .entity = ecs_entity(world, { .name = "DeciBel" }),
        .prefix = EcsDeci,
        .base = EcsBel });
    ecs_primitive_init(world, &(ecs_primitive_desc_t){
        .entity = EcsDeciBel,
        .kind = EcsF32
    });

    /* Documentation */
#ifdef FLECS_DOC
    ECS_IMPORT(world, FlecsDoc);

    ecs_doc_set_brief(world, EcsDuration, 
        "Time amount (e.g. \"20 seconds\", \"2 hours\")");
    ecs_doc_set_brief(world, EcsSeconds, "Time amount in seconds");
    ecs_doc_set_brief(world, EcsMinutes, "60 seconds");
    ecs_doc_set_brief(world, EcsHours, "60 minutes");
    ecs_doc_set_brief(world, EcsDays, "24 hours");

    ecs_doc_set_brief(world, EcsTime,
        "Time passed since an epoch (e.g. \"5pm\", \"March 3rd 2022\")");
    ecs_doc_set_brief(world, EcsDate,
        "Seconds passed since January 1st 1970");

    ecs_doc_set_brief(world, EcsMass, "Units of mass (e.g. \"5 kilograms\")");

    ecs_doc_set_brief(world, EcsElectricCurrent,
        "Units of electrical current (e.g. \"2 ampere\")");

    ecs_doc_set_brief(world, EcsAmount,
        "Units of amount of substance (e.g. \"2 mole\")");

    ecs_doc_set_brief(world, EcsLuminousIntensity,
        "Units of luminous intensity (e.g. \"1 candela\")");

    ecs_doc_set_brief(world, EcsForce, "Units of force (e.g. \"10 newton\")");

    ecs_doc_set_brief(world, EcsLength,
        "Units of length (e.g. \"5 meters\", \"20 miles\")");

    ecs_doc_set_brief(world, EcsPressure, 
        "Units of pressure (e.g. \"1 bar\", \"1000 pascal\")");

    ecs_doc_set_brief(world, EcsSpeed,
        "Units of movement (e.g. \"5 meters/second\")");

    ecs_doc_set_brief(world, EcsAcceleration,
        "Unit of speed increase (e.g. \"5 meters/second/second\")");

    ecs_doc_set_brief(world, EcsTemperature,
        "Units of temperature (e.g. \"5 degrees Celsius\")");

    ecs_doc_set_brief(world, EcsData,
        "Units of information (e.g. \"8 bits\", \"100 megabytes\")");

    ecs_doc_set_brief(world, EcsDataRate,
        "Units of data transmission (e.g. \"100 megabits/second\")");

    ecs_doc_set_brief(world, EcsAngle,
        "Units of rotation (e.g. \"1.2 radians\", \"180 degrees\")");

#endif
}

#endif


#ifdef FLECS_SNAPSHOT


/* World snapshot */
struct ecs_snapshot_t {
    ecs_world_t *world;
    ecs_sparse_t *entity_index;
    ecs_vector_t *tables;
    ecs_entity_t last_id;
    ecs_filter_t filter;
};

/** Small footprint data structure for storing data associated with a table. */
typedef struct ecs_table_leaf_t {
    ecs_table_t *table;
    ecs_type_t type;
    ecs_data_t *data;
} ecs_table_leaf_t;

static
ecs_data_t* duplicate_data(
    ecs_table_t *table,
    ecs_data_t *main_data)
{
    if (!ecs_table_count(table)) {
        return NULL;
    }

    ecs_data_t *result = ecs_os_calloc(ECS_SIZEOF(ecs_data_t));
    int32_t i, column_count = table->storage_count;
    result->columns = ecs_os_memdup_n(
        main_data->columns, ecs_column_t, column_count);

    /* Copy entities and records */
    result->entities = ecs_storage_copy_t(&main_data->entities, ecs_entity_t);
    result->records = ecs_storage_copy_t(&main_data->records, ecs_record_t*);

    /* Copy each column */
    for (i = 0; i < column_count; i ++) {
        ecs_column_t *column = &result->columns[i];
        ecs_type_info_t *ti = table->type_info[i];
        int32_t size = ti->size;
        ecs_copy_t copy = ti->hooks.copy;
        if (copy) {
            ecs_column_t dst = ecs_storage_copy(column, size);
            int32_t count = ecs_storage_count(column);
            void *dst_ptr = ecs_storage_first(&dst);
            void *src_ptr = ecs_storage_first(column);

            ecs_xtor_t ctor = ti->hooks.ctor;
            if (ctor) {
                ctor(dst_ptr, count, ti);
            }

            copy(dst_ptr, src_ptr, count, ti);
            *column = dst;
        } else {
            *column = ecs_storage_copy(column, size);
        }
    }

    return result;
}

static
void snapshot_table(
    ecs_snapshot_t *snapshot,
    ecs_table_t *table)
{
    if (table->flags & EcsTableHasBuiltins) {
        return;
    }
    
    ecs_table_leaf_t *l = ecs_vector_get(
        snapshot->tables, ecs_table_leaf_t, (int32_t)table->id);
    ecs_assert(l != NULL, ECS_INTERNAL_ERROR, NULL);
    
    l->table = table;
    l->type = flecs_type_copy(&table->type);
    l->data = duplicate_data(table, &table->data);
}

static
ecs_snapshot_t* snapshot_create(
    const ecs_world_t *world,
    const ecs_sparse_t *entity_index,
    ecs_iter_t *iter,
    ecs_iter_next_action_t next)
{
    ecs_snapshot_t *result = ecs_os_calloc_t(ecs_snapshot_t);
    ecs_assert(result != NULL, ECS_OUT_OF_MEMORY, NULL);

    ecs_run_aperiodic((ecs_world_t*)world, 0);

    result->world = (ecs_world_t*)world;

    /* If no iterator is provided, the snapshot will be taken of the entire
     * world, and we can simply copy the entity index as it will be restored
     * entirely upon snapshote restore. */
    if (!iter && entity_index) {
        result->entity_index = flecs_sparse_copy(entity_index);
    }

    /* Create vector with as many elements as tables, so we can store the
     * snapshot tables at their element ids. When restoring a snapshot, the code
     * will run a diff between the tables in the world and the snapshot, to see
     * which of the world tables still exist, no longer exist, or need to be
     * deleted. */
    uint64_t t, table_count = flecs_sparse_last_id(&world->store.tables) + 1;
    result->tables = ecs_vector_new(ecs_table_leaf_t, (int32_t)table_count);
    ecs_vector_set_count(&result->tables, ecs_table_leaf_t, (int32_t)table_count);
    ecs_table_leaf_t *arr = ecs_vector_first(result->tables, ecs_table_leaf_t);

    /* Array may have holes, so initialize with 0 */
    ecs_os_memset_n(arr, 0, ecs_table_leaf_t, table_count);

    /* Iterate tables in iterator */
    if (iter) {
        while (next(iter)) {
            ecs_table_t *table = iter->table;
            snapshot_table(result, table);
        }
    } else {
        for (t = 0; t < table_count; t ++) {
            ecs_table_t *table = flecs_sparse_get(
                &world->store.tables, ecs_table_t, t);
            snapshot_table(result, table);
        }
    }

    return result;
}

/** Create a snapshot */
ecs_snapshot_t* ecs_snapshot_take(
    ecs_world_t *stage)
{
    const ecs_world_t *world = ecs_get_world(stage);

    ecs_snapshot_t *result = snapshot_create(
        world, ecs_eis(world), NULL, NULL);

    result->last_id = world->info.last_id;

    return result;
}

/** Create a filtered snapshot */
ecs_snapshot_t* ecs_snapshot_take_w_iter(
    ecs_iter_t *iter)
{
    ecs_world_t *world = iter->world;
    ecs_assert(world != NULL, ECS_INTERNAL_ERROR, NULL);

    ecs_snapshot_t *result = snapshot_create(
        world, ecs_eis(world), iter, iter ? iter->next : NULL);

    result->last_id = world->info.last_id;

    return result;
}

/* Restoring an unfiltered snapshot restores the world to the exact state it was
 * when the snapshot was taken. */
static
void restore_unfiltered(
    ecs_world_t *world,
    ecs_snapshot_t *snapshot)
{
    flecs_sparse_restore(ecs_eis(world), snapshot->entity_index);
    flecs_sparse_free(snapshot->entity_index);
    
    world->info.last_id = snapshot->last_id;

    ecs_table_leaf_t *leafs = ecs_vector_first(
        snapshot->tables, ecs_table_leaf_t);
    int32_t i, count = (int32_t)flecs_sparse_last_id(&world->store.tables);
    int32_t snapshot_count = ecs_vector_count(snapshot->tables);

    for (i = 0; i <= count; i ++) {
        ecs_table_t *world_table = flecs_sparse_get(
            &world->store.tables, ecs_table_t, (uint32_t)i);

        if (world_table && (world_table->flags & EcsTableHasBuiltins)) {
            continue;
        }

        ecs_table_leaf_t *snapshot_table = NULL;
        if (i < snapshot_count) {
            snapshot_table = &leafs[i];
            if (!snapshot_table->table) {
                snapshot_table = NULL;
            }
        }

        /* If the world table no longer exists but the snapshot table does,
         * reinsert it */
        if (!world_table && snapshot_table) {
            ecs_table_t *table = flecs_table_find_or_create(world, 
                &snapshot_table->type);
            ecs_assert(table != NULL, ECS_INTERNAL_ERROR, NULL);

            if (snapshot_table->data) {
                flecs_table_replace_data(world, table, snapshot_table->data);
            }
        
        /* If the world table still exists, replace its data */
        } else if (world_table && snapshot_table) {
            ecs_assert(snapshot_table->table == world_table, 
                ECS_INTERNAL_ERROR, NULL);

            if (snapshot_table->data) {
                flecs_table_replace_data(
                    world, world_table, snapshot_table->data);
            } else {
                flecs_table_clear_data(
                    world, world_table, &world_table->data);
                flecs_table_init_data(world_table);
            }
        
        /* If the snapshot table doesn't exist, this table was created after the
         * snapshot was taken and needs to be deleted */
        } else if (world_table && !snapshot_table) {
            /* Deleting a table invokes OnRemove triggers & updates the entity
             * index. That is not what we want, since entities may no longer be
             * valid (if they don't exist in the snapshot) or may have been
             * restored in a different table. Therefore first clear the data
             * from the table (which doesn't invoke triggers), and then delete
             * the table. */
            flecs_table_clear_data(world, world_table, &world_table->data);
            flecs_delete_table(world, world_table);
        
        /* If there is no world & snapshot table, nothing needs to be done */
        } else { }

        if (snapshot_table) {
            ecs_os_free(snapshot_table->data);
            flecs_type_free(&snapshot_table->type);
        }
    }

    /* Now that all tables have been restored and world is in a consistent
     * state, run OnSet systems */
    int32_t world_count = flecs_sparse_count(&world->store.tables);
    for (i = 0; i < world_count; i ++) {
        ecs_table_t *table = flecs_sparse_get_dense(
            &world->store.tables, ecs_table_t, i);
        if (table->flags & EcsTableHasBuiltins) {
            continue;
        }

        int32_t tcount = ecs_table_count(table);
        if (tcount) {
            flecs_notify_on_set(world, table, 0, tcount, NULL, true);
        }
    }
}

/* Restoring a filtered snapshots only restores the entities in the snapshot
 * to their previous state. */
static
void restore_filtered(
    ecs_world_t *world,
    ecs_snapshot_t *snapshot)
{
    ecs_table_leaf_t *leafs = ecs_vector_first(
        snapshot->tables, ecs_table_leaf_t);
    int32_t l = 0, snapshot_count = ecs_vector_count(snapshot->tables);

    for (l = 0; l < snapshot_count; l ++) {
        ecs_table_leaf_t *snapshot_table = &leafs[l];
        ecs_table_t *table = snapshot_table->table;

        if (!table) {
            continue;
        }

        ecs_data_t *data = snapshot_table->data;
        if (!data) {
            flecs_type_free(&snapshot_table->type);
            continue;
        }

        /* Delete entity from storage first, so that when we restore it to the
         * current table we can be sure that there won't be any duplicates */
        int32_t i, entity_count = ecs_storage_count(&data->entities);
        ecs_entity_t *entities = ecs_storage_first(
            &snapshot_table->data->entities);
        for (i = 0; i < entity_count; i ++) {
            ecs_entity_t e = entities[i];
            ecs_record_t *r = flecs_entities_get(world, e);
            if (r && r->table) {
                flecs_table_delete(world, r->table, 
                    ECS_RECORD_TO_ROW(r->row), true);
            } else {
                /* Make sure that the entity has the same generation count */
                flecs_entities_set_generation(world, e);
            }
        }

        /* Merge data from snapshot table with world table */
        int32_t old_count = ecs_table_count(snapshot_table->table);
        int32_t new_count = flecs_table_data_count(snapshot_table->data);

        flecs_table_merge(world, table, table, &table->data, snapshot_table->data);

        /* Run OnSet systems for merged entities */
        if (new_count) {
            flecs_notify_on_set(
                world, table, old_count, new_count, NULL, true);
        }

        ecs_os_free(snapshot_table->data->columns);
        ecs_os_free(snapshot_table->data);
        flecs_type_free(&snapshot_table->type);
    }
}

/** Restore a snapshot */
void ecs_snapshot_restore(
    ecs_world_t *world,
    ecs_snapshot_t *snapshot)
{
    ecs_run_aperiodic(world, 0);
    
    if (snapshot->entity_index) {
        /* Unfiltered snapshots have a copy of the entity index which is
         * copied back entirely when the snapshot is restored */
        restore_unfiltered(world, snapshot);
    } else {
        restore_filtered(world, snapshot);
    }

    ecs_vector_free(snapshot->tables);   

    ecs_os_free(snapshot);
}

ecs_iter_t ecs_snapshot_iter(
    ecs_snapshot_t *snapshot)
{
    ecs_snapshot_iter_t iter = {
        .tables = snapshot->tables,
        .index = 0
    };

    return (ecs_iter_t){
        .world = snapshot->world,
        .table_count = ecs_vector_count(snapshot->tables),
        .priv.iter.snapshot = iter,
        .next = ecs_snapshot_next
    };
}

bool ecs_snapshot_next(
    ecs_iter_t *it)
{
    ecs_snapshot_iter_t *iter = &it->priv.iter.snapshot;
    ecs_table_leaf_t *tables = ecs_vector_first(iter->tables, ecs_table_leaf_t);
    int32_t count = ecs_vector_count(iter->tables);
    int32_t i;

    for (i = iter->index; i < count; i ++) {
        ecs_table_t *table = tables[i].table;
        if (!table) {
            continue;
        }

        ecs_data_t *data = tables[i].data;

        it->table = table;
        it->count = ecs_table_count(table);
        if (data) {
            it->entities = ecs_storage_first(&data->entities);
        } else {
            it->entities = NULL;
        }

        ECS_BIT_SET(it->flags, EcsIterIsValid);
        iter->index = i + 1;
        
        goto yield;
    }

    ECS_BIT_CLEAR(it->flags, EcsIterIsValid);
    return false;

yield:
    ECS_BIT_CLEAR(it->flags, EcsIterIsValid);
    return true;    
}

/** Cleanup snapshot */
void ecs_snapshot_free(
    ecs_snapshot_t *snapshot)
{
    flecs_sparse_free(snapshot->entity_index);

    ecs_table_leaf_t *tables = ecs_vector_first(snapshot->tables, ecs_table_leaf_t);
    int32_t i, count = ecs_vector_count(snapshot->tables);
    for (i = 0; i < count; i ++) {
        ecs_table_leaf_t *snapshot_table = &tables[i];
        ecs_table_t *table = snapshot_table->table;
        if (table) {
            ecs_data_t *data = snapshot_table->data;
            if (data) {
                flecs_table_clear_data(snapshot->world, table, data);
                ecs_os_free(data);
            }
            flecs_type_free(&snapshot_table->type);
        }
    }    

    ecs_vector_free(snapshot->tables);
    ecs_os_free(snapshot);
}

#endif


#ifdef FLECS_SYSTEM


ecs_mixins_t ecs_system_t_mixins = {
    .type_name = "ecs_system_t",
    .elems = {
        [EcsMixinWorld] = offsetof(ecs_system_t, world),
        [EcsMixinEntity] = offsetof(ecs_system_t, entity),
        [EcsMixinDtor] = offsetof(ecs_system_t, dtor)
    }
};

/* -- Public API -- */

ecs_entity_t ecs_run_intern(
    ecs_world_t *world,
    ecs_stage_t *stage,
    ecs_entity_t system,
    ecs_system_t *system_data,
    int32_t stage_index,
    int32_t stage_count,    
    ecs_ftime_t delta_time,
    int32_t offset,
    int32_t limit,
    void *param) 
{
    ecs_ftime_t time_elapsed = delta_time;
    ecs_entity_t tick_source = system_data->tick_source;

    /* Support legacy behavior */
    if (!param) {
        param = system_data->ctx;
    }

    if (tick_source) {
        const EcsTickSource *tick = ecs_get(
            world, tick_source, EcsTickSource);

        if (tick) {
            time_elapsed = tick->time_elapsed;

            /* If timer hasn't fired we shouldn't run the system */
            if (!tick->tick) {
                return 0;
            }
        } else {
            /* If a timer has been set but the timer entity does not have the
             * EcsTimer component, don't run the system. This can be the result
             * of a single-shot timer that has fired already. Not resetting the
             * timer field of the system will ensure that the system won't be
             * ran after the timer has fired. */
            return 0;
        }
    }

    if (ecs_should_log_3()) {
        char *path = ecs_get_fullpath(world, system);
        ecs_dbg_3("worker %d: %s", stage_index, path);
        ecs_os_free(path);
    }

    ecs_time_t time_start;
    bool measure_time = ECS_BIT_IS_SET(world->flags, EcsWorldMeasureSystemTime);
    if (measure_time) {
        ecs_os_get_time(&time_start);
    }

    ecs_world_t *thread_ctx = world;
    if (stage) {
        thread_ctx = stage->thread_ctx;
    }

    ecs_defer_begin(thread_ctx);

    /* Prepare the query iterator */
    ecs_iter_t pit, wit, qit = ecs_query_iter(thread_ctx, system_data->query);
    ecs_iter_t *it = &qit;

    if (offset || limit) {
        pit = ecs_page_iter(it, offset, limit);
        it = &pit;
    }

    if (stage_count > 1 && system_data->multi_threaded) {
        wit = ecs_worker_iter(it, stage_index, stage_count);
        it = &wit;
    }

    qit.system = system;
    qit.delta_time = delta_time;
    qit.delta_system_time = time_elapsed;
    qit.frame_offset = offset;
    qit.param = param;
    qit.ctx = system_data->ctx;
    qit.binding_ctx = system_data->binding_ctx;

    ecs_iter_action_t action = system_data->action;
    it->callback = action;
    
    ecs_run_action_t run = system_data->run;
    if (run) {
        run(it);
    } else if (system_data->query->filter.term_count) {
        if (it == &qit) {
            while (ecs_query_next(&qit)) {
                action(&qit);
            }
        } else {
            while (ecs_iter_next(it)) {
                action(it);
            }
        }
    } else {
        action(&qit);
    }

    if (measure_time) {
        system_data->time_spent += (float)ecs_time_measure(&time_start);
    }

    system_data->invoke_count ++;

    ecs_defer_end(thread_ctx);

    return it->interrupted_by;
}

/* -- Public API -- */

ecs_entity_t ecs_run_w_filter(
    ecs_world_t *world,
    ecs_entity_t system,
    ecs_ftime_t delta_time,
    int32_t offset,
    int32_t limit,
    void *param)
{
    ecs_stage_t *stage = flecs_stage_from_world(&world);
    ecs_system_t *system_data = ecs_poly_get(world, system, ecs_system_t);
    ecs_assert(system_data != NULL, ECS_INVALID_PARAMETER, NULL);
    return ecs_run_intern(world, stage, system, system_data, 0, 0, delta_time, 
        offset, limit, param);
}

ecs_entity_t ecs_run_worker(
    ecs_world_t *world,
    ecs_entity_t system,
    int32_t stage_index,
    int32_t stage_count,
    ecs_ftime_t delta_time,
    void *param)
{
    ecs_stage_t *stage = flecs_stage_from_world(&world);
    ecs_system_t *system_data = ecs_poly_get(world, system, ecs_system_t);
    ecs_assert(system_data != NULL, ECS_INVALID_PARAMETER, NULL);

    return ecs_run_intern(
        world, stage, system, system_data, stage_index, stage_count, 
        delta_time, 0, 0, param);
}

ecs_entity_t ecs_run(
    ecs_world_t *world,
    ecs_entity_t system,
    ecs_ftime_t delta_time,
    void *param)
{
    return ecs_run_w_filter(world, system, delta_time, 0, 0, param);
}

ecs_query_t* ecs_system_get_query(
    const ecs_world_t *world,
    ecs_entity_t system)
{
    const ecs_system_t *s = ecs_poly_get(world, system, ecs_system_t);
    if (s) {
        return s->query;
    } else {
        return NULL;
    }
}

void* ecs_get_system_ctx(
    const ecs_world_t *world,
    ecs_entity_t system)
{
    const ecs_system_t *s = ecs_poly_get(world, system, ecs_system_t);
    if (s) {
        return s->ctx;
    } else {
        return NULL;
    }   
}

void* ecs_get_system_binding_ctx(
    const ecs_world_t *world,
    ecs_entity_t system)
{
    const ecs_system_t *s = ecs_poly_get(world, system, ecs_system_t);
    if (s) {
        return s->binding_ctx;
    } else {
        return NULL;
    }   
}

/* System deinitialization */
static
void flecs_system_fini(ecs_system_t *sys) {
    if (sys->ctx_free) {
        sys->ctx_free(sys->ctx);
    }

    if (sys->binding_ctx_free) {
        sys->binding_ctx_free(sys->binding_ctx);
    }

    ecs_poly_free(sys, ecs_system_t);
}

ecs_entity_t ecs_system_init(
    ecs_world_t *world,
    const ecs_system_desc_t *desc)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_check(desc != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(desc->_canary == 0, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(!(world->flags & EcsWorldReadonly), 
        ECS_INVALID_WHILE_READONLY, NULL);

    ecs_entity_t entity = desc->entity;
    if (!entity) {
        entity = ecs_new(world, 0);
    }
    EcsPoly *poly = ecs_poly_bind(world, entity, ecs_system_t);
    if (!poly->poly) {
        ecs_system_t *system = ecs_poly_new(ecs_system_t);
        ecs_assert(system != NULL, ECS_INTERNAL_ERROR, NULL);
        
        poly->poly = system;
        system->world = world;
        system->dtor = (ecs_poly_dtor_t)flecs_system_fini;
        system->entity = entity;

        ecs_query_desc_t query_desc = desc->query;
        query_desc.entity = entity;

        ecs_query_t *query = ecs_query_init(world, &query_desc);
        if (!query) {
            ecs_delete(world, entity);
            return 0;
        }

        /* Prevent the system from moving while we're initializing */
        ecs_defer_begin(world);

        system->query = query;
        system->query_entity = query->entity;

        system->run = desc->run;
        system->action = desc->callback;

        system->ctx = desc->ctx;
        system->binding_ctx = desc->binding_ctx;

        system->ctx_free = desc->ctx_free;
        system->binding_ctx_free = desc->binding_ctx_free;

        system->tick_source = desc->tick_source;

        system->multi_threaded = desc->multi_threaded;
        system->no_staging = desc->no_staging;

        if (desc->interval != 0 || desc->rate != 0 || desc->tick_source != 0) {
#ifdef FLECS_TIMER
            if (desc->interval != 0) {
                ecs_set_interval(world, entity, desc->interval);
            }

            if (desc->rate) {
                ecs_set_rate(world, entity, desc->rate, desc->tick_source);
            } else if (desc->tick_source) {
                ecs_set_tick_source(world, entity, desc->tick_source);
            }
#else
            ecs_abort(ECS_UNSUPPORTED, "timer module not available");
#endif
        }

        if (ecs_get_name(world, entity)) {
            ecs_trace("#[green]system#[reset] %s created", 
                ecs_get_name(world, entity));
        }

        ecs_defer_end(world);            
    } else {
        ecs_system_t *system = ecs_poly(poly->poly, ecs_system_t);

        if (desc->run) {
            system->run = desc->run;
        }
        if (desc->callback) {
            system->action = desc->callback;
        }
        if (desc->ctx) {
            system->ctx = desc->ctx;
        }
        if (desc->binding_ctx) {
            system->binding_ctx = desc->binding_ctx;
        }
        if (desc->query.filter.instanced) {
            ECS_BIT_SET(system->query->filter.flags, EcsFilterIsInstanced);
        }
        if (desc->multi_threaded) {
            system->multi_threaded = desc->multi_threaded;
        }
        if (desc->no_staging) {
            system->no_staging = desc->no_staging;
        }
    }

    ecs_poly_modified(world, entity, ecs_system_t);

    return entity;
error:
    return 0;
}

void FlecsSystemImport(
    ecs_world_t *world)
{
    ECS_MODULE(world, FlecsSystem);

    ecs_set_name_prefix(world, "Ecs");

    flecs_bootstrap_tag(world, EcsSystem);
    flecs_bootstrap_component(world, EcsTickSource);

    /* Put following tags in flecs.core so they can be looked up
     * without using the flecs.systems prefix. */
    ecs_entity_t old_scope = ecs_set_scope(world, EcsFlecsCore);
    flecs_bootstrap_tag(world, EcsMonitor);
    ecs_set_scope(world, old_scope);
}

#endif


#ifdef FLECS_JSON

void flecs_json_next(
    ecs_strbuf_t *buf);

void flecs_json_literal(
    ecs_strbuf_t *buf,
    const char *value);

void flecs_json_number(
    ecs_strbuf_t *buf,
    double value);

void flecs_json_true(
    ecs_strbuf_t *buf);

void flecs_json_false(
    ecs_strbuf_t *buf);

void flecs_json_bool(
    ecs_strbuf_t *buf,
    bool value);

void flecs_json_array_push(
    ecs_strbuf_t *buf);

void flecs_json_array_pop(
    ecs_strbuf_t *buf);

void flecs_json_object_push(
    ecs_strbuf_t *buf);

void flecs_json_object_pop(
    ecs_strbuf_t *buf);

void flecs_json_string(
    ecs_strbuf_t *buf,
    const char *value);

void flecs_json_member(
    ecs_strbuf_t *buf,
    const char *name);

void flecs_json_path(
    ecs_strbuf_t *buf,
    const ecs_world_t *world,
    ecs_entity_t e);

void flecs_json_label(
    ecs_strbuf_t *buf,
    const ecs_world_t *world,
    ecs_entity_t e);

void flecs_json_color(
    ecs_strbuf_t *buf,
    const ecs_world_t *world,
    ecs_entity_t e);

void flecs_json_id(
    ecs_strbuf_t *buf,
    const ecs_world_t *world,
    ecs_id_t id);

ecs_primitive_kind_t flecs_json_op_to_primitive_kind(
    ecs_meta_type_op_kind_t kind);

#endif


#ifdef FLECS_JSON

void flecs_json_next(
    ecs_strbuf_t *buf)
{
    ecs_strbuf_list_next(buf);
}

void flecs_json_literal(
    ecs_strbuf_t *buf,
    const char *value)
{
    ecs_strbuf_appendstr(buf, value);
}

void flecs_json_number(
    ecs_strbuf_t *buf,
    double value)
{
    ecs_strbuf_appendflt(buf, value, '"');
}

void flecs_json_true(
    ecs_strbuf_t *buf)
{
    flecs_json_literal(buf, "true");
}

void flecs_json_false(
    ecs_strbuf_t *buf)
{
    flecs_json_literal(buf, "false");
}

void flecs_json_bool(
    ecs_strbuf_t *buf,
    bool value)
{
    if (value) {
        flecs_json_true(buf);
    } else {
        flecs_json_false(buf);
    }
}

void flecs_json_array_push(
    ecs_strbuf_t *buf)
{
    ecs_strbuf_list_push(buf, "[", ", ");
}

void flecs_json_array_pop(
    ecs_strbuf_t *buf)
{
    ecs_strbuf_list_pop(buf, "]");
}

void flecs_json_object_push(
    ecs_strbuf_t *buf)
{
    ecs_strbuf_list_push(buf, "{", ", ");
}

void flecs_json_object_pop(
    ecs_strbuf_t *buf)
{
    ecs_strbuf_list_pop(buf, "}");
}

void flecs_json_string(
    ecs_strbuf_t *buf,
    const char *value)
{
    ecs_strbuf_appendch(buf, '"');
    ecs_strbuf_appendstr(buf, value);
    ecs_strbuf_appendch(buf, '"');
}

void flecs_json_member(
    ecs_strbuf_t *buf,
    const char *name)
{
    ecs_strbuf_list_appendstr(buf, "\"");
    ecs_strbuf_appendstr(buf, name);
    ecs_strbuf_appendstr(buf, "\":");
}

void flecs_json_path(
    ecs_strbuf_t *buf,
    const ecs_world_t *world,
    ecs_entity_t e)
{
    ecs_strbuf_appendch(buf, '"');
    ecs_get_path_w_sep_buf(world, 0, e, ".", "", buf);
    ecs_strbuf_appendch(buf, '"');
}

void flecs_json_label(
    ecs_strbuf_t *buf,
    const ecs_world_t *world,
    ecs_entity_t e)
{
    const char *lbl = NULL;
#ifdef FLECS_DOC
    lbl = ecs_doc_get_name(world, e);
#else
    lbl = ecs_get_name(world, e);
#endif

    if (lbl) {
        ecs_strbuf_appendch(buf, '"');
        ecs_strbuf_appendstr(buf, lbl);
        ecs_strbuf_appendch(buf, '"');
    } else {
        ecs_strbuf_appendstr(buf, "0");
    }
}

void flecs_json_color(
    ecs_strbuf_t *buf,
    const ecs_world_t *world,
    ecs_entity_t e)
{
    (void)world;
    (void)e;

    const char *color = NULL;
#ifdef FLECS_DOC
    color = ecs_doc_get_color(world, e);
#endif

    if (color) {
        ecs_strbuf_appendch(buf, '"');
        ecs_strbuf_appendstr(buf, color);
        ecs_strbuf_appendch(buf, '"');
    } else {
        ecs_strbuf_appendstr(buf, "0");
    }
}

void flecs_json_id(
    ecs_strbuf_t *buf,
    const ecs_world_t *world,
    ecs_id_t id)
{
    ecs_strbuf_appendch(buf, '"');
    ecs_id_str_buf(world, id, buf);
    ecs_strbuf_appendch(buf, '"');
}

ecs_primitive_kind_t flecs_json_op_to_primitive_kind(
    ecs_meta_type_op_kind_t kind) 
{
    return kind - EcsOpPrimitive;
}

#endif


#include <stdio.h>

#ifdef FLECS_JSON

static
int json_ser_type(
    const ecs_world_t *world,
    ecs_vector_t *ser, 
    const void *base, 
    ecs_strbuf_t *str);

static
int json_ser_type_ops(
    const ecs_world_t *world,
    ecs_meta_type_op_t *ops,
    int32_t op_count,
    const void *base, 
    ecs_strbuf_t *str,
    int32_t in_array);

static
int json_ser_type_op(
    const ecs_world_t *world,
    ecs_meta_type_op_t *op, 
    const void *base,
    ecs_strbuf_t *str);

/* Serialize enumeration */
static
int json_ser_enum(
    const ecs_world_t *world,
    ecs_meta_type_op_t *op, 
    const void *base, 
    ecs_strbuf_t *str) 
{
    const EcsEnum *enum_type = ecs_get(world, op->type, EcsEnum);
    ecs_check(enum_type != NULL, ECS_INVALID_PARAMETER, NULL);

    int32_t value = *(int32_t*)base;
    
    /* Enumeration constants are stored in a map that is keyed on the
     * enumeration value. */
    ecs_enum_constant_t *constant = ecs_map_get(
        enum_type->constants, ecs_enum_constant_t, value);
    if (!constant) {
        goto error;
    }

    ecs_strbuf_appendch(str, '"');
    ecs_strbuf_appendstr(str, ecs_get_name(world, constant->constant));
    ecs_strbuf_appendch(str, '"');

    return 0;
error:
    return -1;
}

/* Serialize bitmask */
static
int json_ser_bitmask(
    const ecs_world_t *world,
    ecs_meta_type_op_t *op, 
    const void *ptr, 
    ecs_strbuf_t *str) 
{
    const EcsBitmask *bitmask_type = ecs_get(world, op->type, EcsBitmask);
    ecs_check(bitmask_type != NULL, ECS_INVALID_PARAMETER, NULL);

    uint32_t value = *(uint32_t*)ptr;
    ecs_map_key_t key;
    ecs_bitmask_constant_t *constant;

    if (!value) {
        ecs_strbuf_appendch(str, '0');
        return 0;
    }

    ecs_strbuf_list_push(str, "\"", "|");

    /* Multiple flags can be set at a given time. Iterate through all the flags
     * and append the ones that are set. */
    ecs_map_iter_t it = ecs_map_iter(bitmask_type->constants);
    while ((constant = ecs_map_next(&it, ecs_bitmask_constant_t, &key))) {
        if ((value & key) == key) {
            ecs_strbuf_list_appendstr(str, 
                ecs_get_name(world, constant->constant));
            value -= (uint32_t)key;
        }
    }

    if (value != 0) {
        /* All bits must have been matched by a constant */
        goto error;
    }

    ecs_strbuf_list_pop(str, "\"");

    return 0;
error:
    return -1;
}

/* Serialize elements of a contiguous array */
static
int json_ser_elements(
    const ecs_world_t *world,
    ecs_meta_type_op_t *ops, 
    int32_t op_count,
    const void *base, 
    int32_t elem_count, 
    int32_t elem_size,
    ecs_strbuf_t *str)
{
    flecs_json_array_push(str);

    const void *ptr = base;

    int i;
    for (i = 0; i < elem_count; i ++) {
        ecs_strbuf_list_next(str);
        if (json_ser_type_ops(world, ops, op_count, ptr, str, 1)) {
            return -1;
        }
        ptr = ECS_OFFSET(ptr, elem_size);
    }

    flecs_json_array_pop(str);

    return 0;
}

static
int json_ser_type_elements(
    const ecs_world_t *world,
    ecs_entity_t type, 
    const void *base, 
    int32_t elem_count, 
    ecs_strbuf_t *str)
{
    const EcsMetaTypeSerialized *ser = ecs_get(
        world, type, EcsMetaTypeSerialized);
    ecs_assert(ser != NULL, ECS_INTERNAL_ERROR, NULL);

    const EcsComponent *comp = ecs_get(world, type, EcsComponent);
    ecs_assert(comp != NULL, ECS_INTERNAL_ERROR, NULL);

    ecs_meta_type_op_t *ops = ecs_vector_first(ser->ops, ecs_meta_type_op_t);
    int32_t op_count = ecs_vector_count(ser->ops);

    return json_ser_elements(
        world, ops, op_count, base, elem_count, comp->size, str);
}

/* Serialize array */
static
int json_ser_array(
    const ecs_world_t *world,
    ecs_meta_type_op_t *op, 
    const void *ptr, 
    ecs_strbuf_t *str) 
{
    const EcsArray *a = ecs_get(world, op->type, EcsArray);
    ecs_assert(a != NULL, ECS_INTERNAL_ERROR, NULL);

    return json_ser_type_elements(
        world, a->type, ptr, a->count, str);
}

/* Serialize vector */
static
int json_ser_vector(
    const ecs_world_t *world,
    ecs_meta_type_op_t *op, 
    const void *base, 
    ecs_strbuf_t *str) 
{
    ecs_vector_t *value = *(ecs_vector_t**)base;
    if (!value) {
        ecs_strbuf_appendstr(str, "null");
        return 0;
    }

    const EcsVector *v = ecs_get(world, op->type, EcsVector);
    ecs_assert(v != NULL, ECS_INTERNAL_ERROR, NULL);

    const EcsComponent *comp = ecs_get(world, v->type, EcsComponent);
    ecs_assert(comp != NULL, ECS_INTERNAL_ERROR, NULL);

    int32_t count = ecs_vector_count(value);
    void *array = ecs_vector_first_t(value, comp->size, comp->alignment);

    /* Serialize contiguous buffer of vector */
    return json_ser_type_elements(world, v->type, array, count, str);
}

/* Forward serialization to the different type kinds */
static
int json_ser_type_op(
    const ecs_world_t *world,
    ecs_meta_type_op_t *op, 
    const void *ptr,
    ecs_strbuf_t *str) 
{
    switch(op->kind) {
    case EcsOpPush:
    case EcsOpPop:
        /* Should not be parsed as single op */
        ecs_throw(ECS_INVALID_PARAMETER, NULL);
        break;
    case EcsOpF32:
        ecs_strbuf_appendflt(str, 
            (ecs_f64_t)*(ecs_f32_t*)ECS_OFFSET(ptr, op->offset), '"');
        break;
    case EcsOpF64:
        ecs_strbuf_appendflt(str, 
            *(ecs_f64_t*)ECS_OFFSET(ptr, op->offset), '"');
        break;
    case EcsOpEnum:
        if (json_ser_enum(world, op, ECS_OFFSET(ptr, op->offset), str)) {
            goto error;
        }
        break;
    case EcsOpBitmask:
        if (json_ser_bitmask(world, op, ECS_OFFSET(ptr, op->offset), str)) {
            goto error;
        }
        break;
    case EcsOpArray:
        if (json_ser_array(world, op, ECS_OFFSET(ptr, op->offset), str)) {
            goto error;
        }
        break;
    case EcsOpVector:
        if (json_ser_vector(world, op, ECS_OFFSET(ptr, op->offset), str)) {
            goto error;
        }
        break;
    case EcsOpEntity: {
        ecs_entity_t e = *(ecs_entity_t*)ECS_OFFSET(ptr, op->offset);
        if (!e) {
            ecs_strbuf_appendch(str, '0');
        } else {
            flecs_json_path(str, world, e);
        }
        break;
    }

    default:
        if (ecs_primitive_to_expr_buf(world, 
            flecs_json_op_to_primitive_kind(op->kind), 
            ECS_OFFSET(ptr, op->offset), str)) 
        {
            /* Unknown operation */
            ecs_throw(ECS_INTERNAL_ERROR, NULL);
            return -1;
        }
        break;
    }

    return 0;
error:
    return -1;
}

/* Iterate over a slice of the type ops array */
static
int json_ser_type_ops(
    const ecs_world_t *world,
    ecs_meta_type_op_t *ops,
    int32_t op_count,
    const void *base,
    ecs_strbuf_t *str,
    int32_t in_array)
{
    for (int i = 0; i < op_count; i ++) {
        ecs_meta_type_op_t *op = &ops[i];

        if (in_array <= 0) {
            if (op->name) {
                flecs_json_member(str, op->name);
            }

            int32_t elem_count = op->count;
            if (elem_count > 1) {
                /* Serialize inline array */
                if (json_ser_elements(world, op, op->op_count, base,
                    elem_count, op->size, str))
                {
                    return -1;
                }

                i += op->op_count - 1;
                continue;
            }
        }
        
        switch(op->kind) {
        case EcsOpPush:
            flecs_json_object_push(str);
            in_array --;
            break;
        case EcsOpPop:
            flecs_json_object_pop(str);
            in_array ++;
            break;
        default:
            if (json_ser_type_op(world, op, base, str)) {
                goto error;
            }
            break;
        }
    }

    return 0;
error:
    return -1;
}

/* Iterate over the type ops of a type */
static
int json_ser_type(
    const ecs_world_t *world,
    ecs_vector_t *v_ops,
    const void *base, 
    ecs_strbuf_t *str) 
{
    ecs_meta_type_op_t *ops = ecs_vector_first(v_ops, ecs_meta_type_op_t);
    int32_t count = ecs_vector_count(v_ops);
    return json_ser_type_ops(world, ops, count, base, str, 0);
}

static
int array_to_json_buf_w_type_data(
    const ecs_world_t *world,
    const void *ptr,
    int32_t count,
    ecs_strbuf_t *buf,
    const EcsComponent *comp,
    const EcsMetaTypeSerialized *ser)
{
    if (count) {
        ecs_size_t size = comp->size;

        flecs_json_array_push(buf);

        do {
            ecs_strbuf_list_next(buf);
            if (json_ser_type(world, ser->ops, ptr, buf)) {
                return -1;
            }

            ptr = ECS_OFFSET(ptr, size);
        } while (-- count);

        flecs_json_array_pop(buf);
    } else {
        if (json_ser_type(world, ser->ops, ptr, buf)) {
            return -1;
        }
    }

    return 0;
}

int ecs_array_to_json_buf(
    const ecs_world_t *world,
    ecs_entity_t type,
    const void *ptr,
    int32_t count,
    ecs_strbuf_t *buf)
{
    const EcsComponent *comp = ecs_get(world, type, EcsComponent);
    if (!comp) {
        char *path = ecs_get_fullpath(world, type);
        ecs_err("cannot serialize to JSON, '%s' is not a component", path);
        ecs_os_free(path);
        return -1;
    }

    const EcsMetaTypeSerialized *ser = ecs_get(
        world, type, EcsMetaTypeSerialized);
    if (!ser) {
        char *path = ecs_get_fullpath(world, type);
        ecs_err("cannot serialize to JSON, '%s' has no reflection data", path);
        ecs_os_free(path);
        return -1;
    }

    return array_to_json_buf_w_type_data(world, ptr, count, buf, comp, ser);
}

char* ecs_array_to_json(
    const ecs_world_t *world, 
    ecs_entity_t type, 
    const void* ptr,
    int32_t count)
{
    ecs_strbuf_t str = ECS_STRBUF_INIT;

    if (ecs_array_to_json_buf(world, type, ptr, count, &str) != 0) {
        ecs_strbuf_reset(&str);
        return NULL;
    }

    return ecs_strbuf_get(&str);
}

int ecs_ptr_to_json_buf(
    const ecs_world_t *world,
    ecs_entity_t type,
    const void *ptr,
    ecs_strbuf_t *buf)
{
    return ecs_array_to_json_buf(world, type, ptr, 0, buf);
}

char* ecs_ptr_to_json(
    const ecs_world_t *world, 
    ecs_entity_t type, 
    const void* ptr)
{
    return ecs_array_to_json(world, type, ptr, 0);
}

static
bool skip_id(
    const ecs_world_t *world,
    ecs_id_t id,
    const ecs_entity_to_json_desc_t *desc,
    ecs_entity_t ent,
    ecs_entity_t inst,
    ecs_entity_t *pred_out,
    ecs_entity_t *obj_out,
    ecs_entity_t *role_out,
    bool *hidden_out)
{
    bool is_base = ent != inst;
    ecs_entity_t pred = 0, obj = 0, role = 0;
    bool hidden = false;

    if (ECS_HAS_ROLE(id, PAIR)) {
        pred = ecs_pair_first(world, id);
        obj = ecs_pair_second(world, id);
    } else {
        pred = id & ECS_COMPONENT_MASK;
        if (id & ECS_ROLE_MASK) {
            role = id & ECS_ROLE_MASK;
        }
    }

    if (!desc || !desc->serialize_meta_ids) {
        if (pred == EcsIsA || pred == EcsChildOf ||
            pred == ecs_id(EcsIdentifier)) 
        {
            return true;
        }
#ifdef FLECS_DOC
        if (pred == ecs_id(EcsDocDescription)) {
            return true;
        }
#endif
    }

    if (is_base) {
        if (ecs_has_id(world, pred, EcsDontInherit)) {
            return true;
        }
    }
    if (!desc || !desc->serialize_private) {
        if (ecs_has_id(world, pred, EcsPrivate)) {
            return true;
        }
    }
    if (is_base) {
        if (ecs_get_target_for_id(world, inst, EcsIsA, id) != ent) {
            hidden = true;
        }
    }
    if (hidden && (!desc || !desc->serialize_hidden)) {
        return true;
    }

    *pred_out = pred;
    *obj_out = obj;
    *role_out = role;
    if (hidden_out) *hidden_out = hidden;

    return false;
}

static
int append_type_labels(
    const ecs_world_t *world, 
    ecs_strbuf_t *buf,
    const ecs_id_t *ids,
    int32_t count,
    ecs_entity_t ent, 
    ecs_entity_t inst,
    const ecs_entity_to_json_desc_t *desc) 
{
    (void)world; (void)buf; (void)ids; (void)count; (void)ent; (void)inst; 
    (void)desc;
    
#ifdef FLECS_DOC
    if (!desc || !desc->serialize_id_labels) {
        return 0;
    }

    flecs_json_member(buf, "id_labels");
    flecs_json_array_push(buf);

    int32_t i;
    for (i = 0; i < count; i ++) {
        ecs_entity_t pred = 0, obj = 0, role = 0;
        if (skip_id(world, ids[i], desc, ent, inst, &pred, &obj, &role, 0)) {
            continue;
        }

        if (desc && desc->serialize_id_labels) {
            flecs_json_next(buf);

            flecs_json_array_push(buf);
            flecs_json_next(buf);
            flecs_json_label(buf, world, pred);
            if (obj) {
                flecs_json_next(buf);
                flecs_json_label(buf, world, obj);
            }

            flecs_json_array_pop(buf);
        }
    }

    flecs_json_array_pop(buf);
#endif
    return 0;
}

static
int append_type_values(
    const ecs_world_t *world, 
    ecs_strbuf_t *buf, 
    const ecs_id_t *ids,
    int32_t count,
    ecs_entity_t ent, 
    ecs_entity_t inst,
    const ecs_entity_to_json_desc_t *desc) 
{
    if (!desc || !desc->serialize_values) {
        return 0;
    }

    flecs_json_member(buf, "values");
    flecs_json_array_push(buf);

    int32_t i;
    for (i = 0; i < count; i ++) {
        bool hidden;
        ecs_entity_t pred = 0, obj = 0, role = 0;
        ecs_id_t id = ids[i];
        if (skip_id(world, id, desc, ent, inst, &pred, &obj, &role, 
            &hidden)) 
        {
            continue;
        }

        if (!hidden) {
            bool serialized = false;
            ecs_entity_t typeid = ecs_get_typeid(world, id);
            if (typeid) {
                const EcsMetaTypeSerialized *ser = ecs_get(
                    world, typeid, EcsMetaTypeSerialized);
                if (ser) {
                    const void *ptr = ecs_get_id(world, ent, id);
                    ecs_assert(ptr != NULL, ECS_INTERNAL_ERROR, NULL);

                    flecs_json_next(buf);
                    if (json_ser_type(world, ser->ops, ptr, buf) != 0) {
                        /* Entity contains invalid value */
                        return -1;
                    }
                    serialized = true;
                }
            }
            if (!serialized) {
                flecs_json_next(buf);
                flecs_json_number(buf, 0);
            }
        } else {
            if (!desc || desc->serialize_hidden) {
                flecs_json_next(buf);
                flecs_json_number(buf, 0);
            }
        }
    }

    flecs_json_array_pop(buf);
    
    return 0;
}

static
int append_type_info(
    const ecs_world_t *world, 
    ecs_strbuf_t *buf, 
    const ecs_id_t *ids,
    int32_t count,
    ecs_entity_t ent, 
    ecs_entity_t inst,
    const ecs_entity_to_json_desc_t *desc) 
{
    if (!desc || !desc->serialize_type_info) {
        return 0;
    }

    flecs_json_member(buf, "type_info");
    flecs_json_array_push(buf);

    int32_t i;
    for (i = 0; i < count; i ++) {
        bool hidden;
        ecs_entity_t pred = 0, obj = 0, role = 0;
        ecs_id_t id = ids[i];
        if (skip_id(world, id, desc, ent, inst, &pred, &obj, &role, 
            &hidden)) 
        {
            continue;
        }

        if (!hidden) {
            ecs_entity_t typeid = ecs_get_typeid(world, id);
            if (typeid) {
                flecs_json_next(buf);
                if (ecs_type_info_to_json_buf(world, typeid, buf) != 0) {
                    return -1;
                }
            } else {
                flecs_json_next(buf);
                flecs_json_number(buf, 0);
            }
        } else {
            if (!desc || desc->serialize_hidden) {
                flecs_json_next(buf);
                flecs_json_number(buf, 0);
            }
        }
    }

    flecs_json_array_pop(buf);
    
    return 0;
}

static
int append_type_hidden(
    const ecs_world_t *world, 
    ecs_strbuf_t *buf, 
    const ecs_id_t *ids,
    int32_t count,
    ecs_entity_t ent, 
    ecs_entity_t inst,
    const ecs_entity_to_json_desc_t *desc) 
{
    if (!desc || !desc->serialize_hidden) {
        return 0;
    }

    if (ent == inst) {
        return 0; /* if this is not a base, components are never hidden */
    }

    flecs_json_member(buf, "hidden");
    flecs_json_array_push(buf);

    int32_t i;
    for (i = 0; i < count; i ++) {
        bool hidden;
        ecs_entity_t pred = 0, obj = 0, role = 0;
        ecs_id_t id = ids[i];
        if (skip_id(world, id, desc, ent, inst, &pred, &obj, &role, 
            &hidden)) 
        {
            continue;
        }

        flecs_json_next(buf);
        flecs_json_bool(buf, hidden);
    }

    flecs_json_array_pop(buf);
    
    return 0;
}


static
int append_type(
    const ecs_world_t *world, 
    ecs_strbuf_t *buf, 
    ecs_entity_t ent, 
    ecs_entity_t inst,
    const ecs_entity_to_json_desc_t *desc) 
{
    const ecs_id_t *ids = NULL;
    int32_t i, count = 0;

    const ecs_type_t *type = ecs_get_type(world, ent);
    if (type) {
        ids = type->array;
        count = type->count;
    }

    flecs_json_member(buf, "ids");
    flecs_json_array_push(buf);

    for (i = 0; i < count; i ++) {
        ecs_entity_t pred = 0, obj = 0, role = 0;
        if (skip_id(world, ids[i], desc, ent, inst, &pred, &obj, &role, 0)) {
            continue;
        }


        flecs_json_next(buf);
        flecs_json_array_push(buf);
        flecs_json_next(buf);
        flecs_json_path(buf, world, pred);
        if (obj || role) {
            flecs_json_next(buf);
            if (obj) {
                flecs_json_path(buf, world, obj);
            } else {
                flecs_json_number(buf, 0);
            }
            if (role) {
                flecs_json_next(buf);
                flecs_json_string(buf, ecs_role_str(role));
            }
        }
        flecs_json_array_pop(buf);
    }

    flecs_json_array_pop(buf);

    if (append_type_labels(world, buf, ids, count, ent, inst, desc)) {
        return -1;
    }
    
    if (append_type_values(world, buf, ids, count, ent, inst, desc)) {
        return -1;
    }

    if (append_type_info(world, buf, ids, count, ent, inst, desc)) {
        return -1;
    }

    if (append_type_hidden(world, buf, ids, count, ent, inst, desc)) {
        return -1;
    }

    return 0;
}

static
int append_base(
    const ecs_world_t *world, 
    ecs_strbuf_t *buf, 
    ecs_entity_t ent, 
    ecs_entity_t inst,
    const ecs_entity_to_json_desc_t *desc) 
{
    const ecs_type_t *type = ecs_get_type(world, ent);
    ecs_id_t *ids = NULL;
    int32_t i, count = 0;
    if (type) {
        ids = type->array;
        count = type->count;
    }

    for (i = 0; i < count; i ++) {
        ecs_id_t id = ids[i];
        if (ECS_HAS_RELATION(id, EcsIsA)) {
            if (append_base(world, buf, ecs_pair_second(world, id), inst, desc)) 
            {
                return -1;
            }
        }
    }

    ecs_strbuf_list_next(buf);
    flecs_json_object_push(buf);
    flecs_json_member(buf, "path");
    flecs_json_path(buf, world, ent);

    if (append_type(world, buf, ent, inst, desc)) {
        return -1;
    }

    flecs_json_object_pop(buf);

    return 0;
}

int ecs_entity_to_json_buf(
    const ecs_world_t *world,
    ecs_entity_t entity,
    ecs_strbuf_t *buf,
    const ecs_entity_to_json_desc_t *desc)
{
    if (!entity || !ecs_is_valid(world, entity)) {
        return -1;
    }

    flecs_json_object_push(buf);

    if (!desc || desc->serialize_path) {
        char *path = ecs_get_fullpath(world, entity);
        flecs_json_member(buf, "path");
        flecs_json_string(buf, path);
        ecs_os_free(path);
    }

#ifdef FLECS_DOC
    if (desc && desc->serialize_label) {
        flecs_json_member(buf, "label");
        const char *doc_name = ecs_doc_get_name(world, entity);
        if (doc_name) {
            flecs_json_string(buf, doc_name);
        } else {
            char num_buf[20];
            ecs_os_sprintf(num_buf, "%u", (uint32_t)entity);
            flecs_json_string(buf, num_buf);
        }
    }

    if (desc && desc->serialize_brief) {
        const char *doc_brief = ecs_doc_get_brief(world, entity);
        if (doc_brief) {
            flecs_json_member(buf, "brief");
            flecs_json_string(buf, doc_brief);
        }
    }

    if (desc && desc->serialize_link) {
        const char *doc_link = ecs_doc_get_link(world, entity);
        if (doc_link) {
            flecs_json_member(buf, "link");
            flecs_json_string(buf, doc_link);
        }
    }

    if (desc && desc->serialize_color) {
        const char *doc_color = ecs_doc_get_color(world, entity);
        if (doc_color) {
            flecs_json_member(buf, "color");
            flecs_json_string(buf, doc_color);
        }
    }
#endif

    const ecs_type_t *type = ecs_get_type(world, entity);
    ecs_id_t *ids = NULL;
    int32_t i, count = 0;
    if (type) {
        ids = type->array;
        count = type->count;
    }

    if (!desc || desc->serialize_base) {
        if (ecs_has_pair(world, entity, EcsIsA, EcsWildcard)) {
            flecs_json_member(buf, "is_a");
            flecs_json_array_push(buf);

            for (i = 0; i < count; i ++) {
                ecs_id_t id = ids[i];
                if (ECS_HAS_RELATION(id, EcsIsA)) {
                    if (append_base(
                        world, buf, ecs_pair_second(world, id), entity, desc)) 
                    {
                        return -1;
                    }
                }
            }

            flecs_json_array_pop(buf);
        }
    }

    if (append_type(world, buf, entity, entity, desc)) {
        goto error;
    }

    flecs_json_object_pop(buf);

    return 0;
error:
    return -1;
}

char* ecs_entity_to_json(
    const ecs_world_t *world,
    ecs_entity_t entity,
    const ecs_entity_to_json_desc_t *desc)
{
    ecs_strbuf_t buf = ECS_STRBUF_INIT;

    if (ecs_entity_to_json_buf(world, entity, &buf, desc) != 0) {
        ecs_strbuf_reset(&buf);
        return NULL;
    }

    return ecs_strbuf_get(&buf);
}

static
bool skip_variable(
    const char *name)
{
    if (!name || name[0] == '_' || name[0] == '.') {
        return true;
    } else {
        return false;
    }
}

static
void serialize_id(
    const ecs_world_t *world,
    ecs_id_t id,
    ecs_strbuf_t *buf) 
{
    flecs_json_id(buf, world, id);
}

static
void serialize_iter_ids(
    const ecs_world_t *world,
    const ecs_iter_t *it, 
    ecs_strbuf_t *buf) 
{
    int32_t term_count = it->term_count;
    if (!term_count) {
        return;
    }

    flecs_json_member(buf, "ids");
    flecs_json_array_push(buf);

    for (int i = 0; i < term_count; i ++) {
        flecs_json_next(buf);
        serialize_id(world, it->terms[i].id, buf);
    }

    flecs_json_array_pop(buf);
}

static
void serialize_type_info(
    const ecs_world_t *world,
    const ecs_iter_t *it, 
    ecs_strbuf_t *buf) 
{
    int32_t term_count = it->term_count;
    if (!term_count) {
        return;
    }

    flecs_json_member(buf, "type_info");
    flecs_json_object_push(buf);

    for (int i = 0; i < term_count; i ++) {
        flecs_json_next(buf);
        ecs_entity_t typeid = ecs_get_typeid(world, it->terms[i].id);
        if (typeid) {
            serialize_id(world, typeid, buf);
            ecs_strbuf_appendstr(buf, ":");
            ecs_type_info_to_json_buf(world, typeid, buf);
        } else {
            serialize_id(world, it->terms[i].id, buf);
            ecs_strbuf_appendstr(buf, ":");
            ecs_strbuf_appendstr(buf, "0");
        }
    }

    flecs_json_object_pop(buf);
}

static
void serialize_iter_variables(ecs_iter_t *it, ecs_strbuf_t *buf) {
    char **variable_names = it->variable_names;
    int32_t var_count = it->variable_count;
    int32_t actual_count = 0;

    for (int i = 0; i < var_count; i ++) {
        const char *var_name = variable_names[i];
        if (skip_variable(var_name)) continue;

        if (!actual_count) {
            flecs_json_member(buf, "vars");
            flecs_json_array_push(buf);
            actual_count ++;
        }

        ecs_strbuf_list_next(buf);
        flecs_json_string(buf, var_name);
    }

    if (actual_count) {
        flecs_json_array_pop(buf);
    }
}

static
void serialize_iter_result_ids(
    const ecs_world_t *world,
    const ecs_iter_t *it,
    ecs_strbuf_t *buf)
{
    flecs_json_member(buf, "ids");
    flecs_json_array_push(buf);

    for (int i = 0; i < it->term_count; i ++) {
        flecs_json_next(buf);
        serialize_id(world,  ecs_field_id(it, i + 1), buf);
    }

    flecs_json_array_pop(buf);
}

static
void serialize_iter_result_sources(
    const ecs_world_t *world,
    const ecs_iter_t *it,
    ecs_strbuf_t *buf)
{
    flecs_json_member(buf, "sources");
    flecs_json_array_push(buf);

    for (int i = 0; i < it->term_count; i ++) {
        flecs_json_next(buf);
        ecs_entity_t subj = it->sources[i];
        if (subj) {            
            flecs_json_path(buf, world, subj);
        } else {
            flecs_json_literal(buf, "0");
        }
    }

    flecs_json_array_pop(buf);
}

static
void serialize_iter_result_is_set(
    const ecs_iter_t *it,
    ecs_strbuf_t *buf)
{
    flecs_json_member(buf, "is_set");
    flecs_json_array_push(buf);

    for (int i = 0; i < it->term_count; i ++) {
        ecs_strbuf_list_next(buf);
        if (ecs_field_is_set(it, i + 1)) {
            flecs_json_true(buf);
        } else {
            flecs_json_false(buf);
        }
    }

    flecs_json_array_pop(buf);
}

static
void serialize_iter_result_variables(
    const ecs_world_t *world,
    const ecs_iter_t *it,
    ecs_strbuf_t *buf) 
{
    char **variable_names = it->variable_names;
    ecs_var_t *variables = it->variables;
    int32_t var_count = it->variable_count;
    int32_t actual_count = 0;

    for (int i = 0; i < var_count; i ++) {
        const char *var_name = variable_names[i];
        if (skip_variable(var_name)) continue;

        if (!actual_count) {
            flecs_json_member(buf, "vars");
            flecs_json_array_push(buf);
            actual_count ++;
        }

        ecs_strbuf_list_next(buf);
        flecs_json_path(buf, world, variables[i].entity);
    }

    if (actual_count) {
        flecs_json_array_pop(buf);
    }
}

static
void serialize_iter_result_variable_labels(
    const ecs_world_t *world,
    const ecs_iter_t *it,
    ecs_strbuf_t *buf) 
{
    char **variable_names = it->variable_names;
    ecs_var_t *variables = it->variables;
    int32_t var_count = it->variable_count;
    int32_t actual_count = 0;

    for (int i = 0; i < var_count; i ++) {
        const char *var_name = variable_names[i];
        if (skip_variable(var_name)) continue;

        if (!actual_count) {
            flecs_json_member(buf, "var_labels");
            flecs_json_array_push(buf);
            actual_count ++;
        }

        ecs_strbuf_list_next(buf);
        flecs_json_label(buf, world, variables[i].entity);
    }

    if (actual_count) {
        flecs_json_array_pop(buf);
    }
}

static
void serialize_iter_result_variable_ids(
    const ecs_iter_t *it,
    ecs_strbuf_t *buf) 
{
    char **variable_names = it->variable_names;
    ecs_var_t *variables = it->variables;
    int32_t var_count = it->variable_count;
    int32_t actual_count = 0;

    for (int i = 0; i < var_count; i ++) {
        const char *var_name = variable_names[i];
        if (skip_variable(var_name)) continue;

        if (!actual_count) {
            flecs_json_member(buf, "var_ids");
            flecs_json_array_push(buf);
            actual_count ++;
        }

        ecs_strbuf_list_next(buf);
        flecs_json_number(buf, (double)variables[i].entity);
    }

    if (actual_count) {
        flecs_json_array_pop(buf);
    }
}

static
void serialize_iter_result_entities(
    const ecs_world_t *world,
    const ecs_iter_t *it,
    ecs_strbuf_t *buf) 
{
    int32_t count = it->count;
    if (!it->count) {
        return;
    }

    flecs_json_member(buf, "entities");
    flecs_json_array_push(buf);

    ecs_entity_t *entities = it->entities;

    for (int i = 0; i < count; i ++) {
        flecs_json_next(buf);
        flecs_json_path(buf, world, entities[i]);
    }

    flecs_json_array_pop(buf);
}

static
void serialize_iter_result_entity_labels(
    const ecs_world_t *world,
    const ecs_iter_t *it,
    ecs_strbuf_t *buf) 
{
    int32_t count = it->count;
    if (!it->count) {
        return;
    }

    flecs_json_member(buf, "entity_labels");
    flecs_json_array_push(buf);

    ecs_entity_t *entities = it->entities;

    for (int i = 0; i < count; i ++) {
        flecs_json_next(buf);
        flecs_json_label(buf, world, entities[i]);
    }

    flecs_json_array_pop(buf);
}

static
void serialize_iter_result_entity_ids(
    const ecs_iter_t *it,
    ecs_strbuf_t *buf) 
{
    int32_t count = it->count;
    if (!it->count) {
        return;
    }

    flecs_json_member(buf, "entity_ids");
    flecs_json_array_push(buf);

    ecs_entity_t *entities = it->entities;

    for (int i = 0; i < count; i ++) {
        flecs_json_next(buf);
        flecs_json_number(buf, (double)entities[i]);
    }

    flecs_json_array_pop(buf);
}

static
void serialize_iter_result_colors(
    const ecs_world_t *world,
    const ecs_iter_t *it,
    ecs_strbuf_t *buf) 
{
    int32_t count = it->count;
    if (!it->count) {
        return;
    }

    flecs_json_member(buf, "colors");
    flecs_json_array_push(buf);

    ecs_entity_t *entities = it->entities;

    for (int i = 0; i < count; i ++) {
        flecs_json_next(buf);
        flecs_json_color(buf, world, entities[i]);
    }

    flecs_json_array_pop(buf);
}

static
void serialize_iter_result_values(
    const ecs_world_t *world,
    const ecs_iter_t *it,
    ecs_strbuf_t *buf) 
{
    flecs_json_member(buf, "values");
    flecs_json_array_push(buf);

    int32_t i, term_count = it->term_count;
    for (i = 0; i < term_count; i ++) {
        ecs_strbuf_list_next(buf);

        const void *ptr = NULL;
        if (it->ptrs) {
            ptr = it->ptrs[i];
        }

        if (!ptr) {
            /* No data in column. Append 0 if this is not an optional term */
            if (ecs_field_is_set(it, i + 1)) {
                flecs_json_literal(buf, "0");
                continue;
            }
        }

        if (ecs_field_is_writeonly(it, i + 1)) {
            flecs_json_literal(buf, "0");
            continue;
        }

        /* Get component id (can be different in case of pairs) */
        ecs_entity_t type = ecs_get_typeid(world, it->ids[i]);
        if (!type) {
            /* Odd, we have a ptr but no Component? Not the place of the
             * serializer to complain about that. */
            flecs_json_literal(buf, "0");
            continue;
        }

        const EcsComponent *comp = ecs_get(world, type, EcsComponent);
        if (!comp) {
            /* Also odd, typeid but not a component? */
            flecs_json_literal(buf, "0");
            continue;
        }

        const EcsMetaTypeSerialized *ser = ecs_get(
            world, type, EcsMetaTypeSerialized);
        if (!ser) {
            /* Not odd, component just has no reflection data */
            flecs_json_literal(buf, "0");
            continue;
        }

        /* If term is not set, append empty array. This indicates that the term
         * could have had data but doesn't */
        if (!ecs_field_is_set(it, i + 1)) {
            ecs_assert(ptr == NULL, ECS_INTERNAL_ERROR, NULL);
            flecs_json_array_push(buf);
            flecs_json_array_pop(buf);
            continue;
        }

        if (ecs_field_is_self(it, i + 1)) {
            int32_t count = it->count;
            array_to_json_buf_w_type_data(world, ptr, count, buf, comp, ser);
        } else {
            array_to_json_buf_w_type_data(world, ptr, 0, buf, comp, ser);
        }
    }

    flecs_json_array_pop(buf);
}

static
void serialize_iter_result(
    const ecs_world_t *world, 
    const ecs_iter_t *it, 
    ecs_strbuf_t *buf,
    const ecs_iter_to_json_desc_t *desc) 
{
    flecs_json_next(buf);
    flecs_json_object_push(buf);

    /* Each result can be matched with different component ids. Add them to
     * the result so clients know with which component an entity was matched */
    if (!desc || desc->serialize_ids) {
        serialize_iter_result_ids(world, it, buf);
    }

    /* Include information on which entity the term is matched with */
    if (!desc || desc->serialize_ids) {
        serialize_iter_result_sources(world, it, buf);
    }

    /* Write variable values for current result */
    if (!desc || desc->serialize_variables) {
        serialize_iter_result_variables(world, it, buf);
    }

    /* Write labels for variables */
    if (desc && desc->serialize_variable_labels) {
        serialize_iter_result_variable_labels(world, it, buf);
    }

    /* Write ids for variables */
    if (desc && desc->serialize_variable_ids) {
        serialize_iter_result_variable_ids(it, buf);
    }

    /* Include information on which terms are set, to support optional terms */
    if (!desc || desc->serialize_is_set) {
        serialize_iter_result_is_set(it, buf);
    }

    /* Write entity ids for current result (for queries with This terms) */
    if (!desc || desc->serialize_entities) {
        serialize_iter_result_entities(world, it, buf);
    }

    /* Write labels for entities */
    if (desc && desc->serialize_entity_labels) {
        serialize_iter_result_entity_labels(world, it, buf);
    }

    /* Write ids for entities */
    if (desc && desc->serialize_entity_ids) {
        serialize_iter_result_entity_ids(it, buf);
    }

    /* Write colors for entities */
    if (desc && desc->serialize_colors) {
        serialize_iter_result_colors(world, it, buf);
    }

    /* Serialize component values */
    if (!desc || desc->serialize_values) {
        serialize_iter_result_values(world, it, buf);
    }

    flecs_json_object_pop(buf);
}

int ecs_iter_to_json_buf(
    const ecs_world_t *world,
    ecs_iter_t *it,
    ecs_strbuf_t *buf,
    const ecs_iter_to_json_desc_t *desc)
{
    ecs_time_t duration = {0};
    if (desc && desc->measure_eval_duration) {
        ecs_time_measure(&duration);
    }

    flecs_json_object_push(buf);

    /* Serialize component ids of the terms (usually provided by query) */
    if (!desc || desc->serialize_term_ids) {
        serialize_iter_ids(world, it, buf);
    }

    /* Serialize type info if enabled */
    if (desc && desc->serialize_type_info) {
        serialize_type_info(world, it, buf);
    }

    /* Serialize variable names, if iterator has any */
    serialize_iter_variables(it, buf);

    /* Serialize results */
    flecs_json_member(buf, "results");
    flecs_json_array_push(buf);

    /* Use instancing for improved performance */
    ECS_BIT_SET(it->flags, EcsIterIsInstanced);

    ecs_iter_next_action_t next = it->next;
    while (next(it)) {
        serialize_iter_result(world, it, buf, desc);
    }

    flecs_json_array_pop(buf);

    if (desc && desc->measure_eval_duration) {
        double dt = ecs_time_measure(&duration);
        flecs_json_member(buf, "eval_duration");
        flecs_json_number(buf, dt);
    }

    flecs_json_object_pop(buf);

    return 0;
}

char* ecs_iter_to_json(
    const ecs_world_t *world,
    ecs_iter_t *it,
    const ecs_iter_to_json_desc_t *desc)
{
    ecs_strbuf_t buf = ECS_STRBUF_INIT;

    if (ecs_iter_to_json_buf(world, it, &buf, desc)) {
        ecs_strbuf_reset(&buf);
        return NULL;
    }

    return ecs_strbuf_get(&buf);
}

#endif


#ifdef FLECS_JSON

static
int json_typeinfo_ser_type(
    const ecs_world_t *world,
    ecs_entity_t type,
    ecs_strbuf_t *buf);

static
int json_typeinfo_ser_primitive(
    ecs_primitive_kind_t kind,
    ecs_strbuf_t *str) 
{
    switch(kind) {
    case EcsBool:
        flecs_json_string(str, "bool");
        break;
    case EcsChar:
    case EcsString:
        flecs_json_string(str, "text");
        break;
    case EcsByte:
        flecs_json_string(str, "byte");
        break;
    case EcsU8:
    case EcsU16:
    case EcsU32:
    case EcsU64:
    case EcsI8:
    case EcsI16:
    case EcsI32:
    case EcsI64:
    case EcsIPtr:
    case EcsUPtr:
        flecs_json_string(str, "int");
        break;
    case EcsF32:
    case EcsF64:
        flecs_json_string(str, "float");
        break;
    case EcsEntity:
        flecs_json_string(str, "entity");
        break;
    default:
        return -1;
    }

    return 0;
}

static
void json_typeinfo_ser_constants(
    const ecs_world_t *world,
    ecs_entity_t type,
    ecs_strbuf_t *str)
{
    ecs_iter_t it = ecs_term_iter(world, &(ecs_term_t) {
        .id = ecs_pair(EcsChildOf, type)
    });

    while (ecs_term_next(&it)) {
        int32_t i, count = it.count;
        for (i = 0; i < count; i ++) {
            flecs_json_next(str);
            flecs_json_string(str, ecs_get_name(world, it.entities[i]));
        }
    }
}

static
void json_typeinfo_ser_enum(
    const ecs_world_t *world,
    ecs_entity_t type,
    ecs_strbuf_t *str)
{
    ecs_strbuf_list_appendstr(str, "\"enum\"");
    json_typeinfo_ser_constants(world, type, str);
}

static
void json_typeinfo_ser_bitmask(
    const ecs_world_t *world,
    ecs_entity_t type,
    ecs_strbuf_t *str)
{
    ecs_strbuf_list_appendstr(str, "\"bitmask\"");
    json_typeinfo_ser_constants(world, type, str);
}

static
int json_typeinfo_ser_array(
    const ecs_world_t *world,
    ecs_entity_t elem_type,
    int32_t count,
    ecs_strbuf_t *str)
{
    ecs_strbuf_list_appendstr(str, "\"array\"");

    flecs_json_next(str);
    if (json_typeinfo_ser_type(world, elem_type, str)) {
        goto error;
    }

    ecs_strbuf_list_append(str, "%u", count);
    return 0;
error:
    return -1;
}

static
int json_typeinfo_ser_array_type(
    const ecs_world_t *world,
    ecs_entity_t type,
    ecs_strbuf_t *str)
{
    const EcsArray *arr = ecs_get(world, type, EcsArray);
    ecs_assert(arr != NULL, ECS_INTERNAL_ERROR, NULL);
    if (json_typeinfo_ser_array(world, arr->type, arr->count, str)) {
        goto error;
    }

    return 0;
error:
    return -1;
}

static
int json_typeinfo_ser_vector(
    const ecs_world_t *world,
    ecs_entity_t type,
    ecs_strbuf_t *str)
{
    const EcsVector *arr = ecs_get(world, type, EcsVector);
    ecs_assert(arr != NULL, ECS_INTERNAL_ERROR, NULL);

    ecs_strbuf_list_appendstr(str, "\"vector\"");

    flecs_json_next(str);
    if (json_typeinfo_ser_type(world, arr->type, str)) {
        goto error;
    }

    return 0;
error:
    return -1;
}

/* Serialize unit information */
static
int json_typeinfo_ser_unit(
    const ecs_world_t *world,
    ecs_strbuf_t *str,
    ecs_entity_t unit) 
{
    flecs_json_member(str, "unit");
    flecs_json_path(str, world, unit);

    const EcsUnit *uptr = ecs_get(world, unit, EcsUnit);
    if (uptr) {
        if (uptr->symbol) {
            flecs_json_member(str, "symbol");
            flecs_json_string(str, uptr->symbol);
        }
        ecs_entity_t quantity = ecs_get_target(world, unit, EcsQuantity, 0);
        if (quantity) {
            flecs_json_member(str, "quantity");
            flecs_json_path(str, world, quantity);
        }
    }

    return 0;
}

/* Forward serialization to the different type kinds */
static
int json_typeinfo_ser_type_op(
    const ecs_world_t *world,
    ecs_meta_type_op_t *op, 
    ecs_strbuf_t *str) 
{
    flecs_json_array_push(str);

    switch(op->kind) {
    case EcsOpPush:
    case EcsOpPop:
        /* Should not be parsed as single op */
        ecs_throw(ECS_INVALID_PARAMETER, NULL);
        break;
    case EcsOpEnum:
        json_typeinfo_ser_enum(world, op->type, str);
        break;
    case EcsOpBitmask:
        json_typeinfo_ser_bitmask(world, op->type, str);
        break;
    case EcsOpArray:
        json_typeinfo_ser_array_type(world, op->type, str);
        break;
    case EcsOpVector:
        json_typeinfo_ser_vector(world, op->type, str);
        break;
    default:
        if (json_typeinfo_ser_primitive( 
            flecs_json_op_to_primitive_kind(op->kind), str))
        {
            /* Unknown operation */
            ecs_throw(ECS_INTERNAL_ERROR, NULL);
            return -1;
        }
        break;
    }

    ecs_entity_t unit = op->unit;
    if (unit) {
        flecs_json_next(str);
        flecs_json_next(str);

        flecs_json_object_push(str);
        json_typeinfo_ser_unit(world, str, unit);
        flecs_json_object_pop(str);
    }

    flecs_json_array_pop(str);

    return 0;
error:
    return -1;
}

/* Iterate over a slice of the type ops array */
static
int json_typeinfo_ser_type_ops(
    const ecs_world_t *world,
    ecs_meta_type_op_t *ops,
    int32_t op_count,
    ecs_strbuf_t *str) 
{
    for (int i = 0; i < op_count; i ++) {
        ecs_meta_type_op_t *op = &ops[i];

        if (op != ops) {
            if (op->name) {
                flecs_json_member(str, op->name);
            }

            int32_t elem_count = op->count;
            if (elem_count > 1 && op != ops) {
                flecs_json_array_push(str);
                json_typeinfo_ser_array(world, op->type, op->count, str);
                flecs_json_array_pop(str);
                i += op->op_count - 1;
                continue;
            }
        }
        
        switch(op->kind) {
        case EcsOpPush:
            flecs_json_object_push(str);
            break;
        case EcsOpPop:
            flecs_json_object_pop(str);
            break;
        default:
            if (json_typeinfo_ser_type_op(world, op, str)) {
                goto error;
            }
            break;
        }
    }

    return 0;
error:
    return -1;
}

static
int json_typeinfo_ser_type(
    const ecs_world_t *world,
    ecs_entity_t type,
    ecs_strbuf_t *buf)
{
    const EcsComponent *comp = ecs_get(world, type, EcsComponent);
    if (!comp) {
        ecs_strbuf_appendstr(buf, "0");
        return 0;
    }

    const EcsMetaTypeSerialized *ser = ecs_get(
        world, type, EcsMetaTypeSerialized);
    if (!ser) {
        ecs_strbuf_appendstr(buf, "0");
        return 0;
    }

    ecs_meta_type_op_t *ops = ecs_vector_first(ser->ops, ecs_meta_type_op_t);
    int32_t count = ecs_vector_count(ser->ops);

    return json_typeinfo_ser_type_ops(world, ops, count, buf);
}

int ecs_type_info_to_json_buf(
    const ecs_world_t *world,
    ecs_entity_t type,
    ecs_strbuf_t *buf)
{
    return json_typeinfo_ser_type(world, type, buf);
}

char* ecs_type_info_to_json(
    const ecs_world_t *world,
    ecs_entity_t type)
{
    ecs_strbuf_t str = ECS_STRBUF_INIT;

    if (ecs_type_info_to_json_buf(world, type, &str) != 0) {
        ecs_strbuf_reset(&str);
        return NULL;
    }

    return ecs_strbuf_get(&str);
}

#endif



#ifdef FLECS_JSON

const char* ecs_parse_json(
    const ecs_world_t *world,
    const char *ptr,
    ecs_entity_t type,
    void *data_out,
    const ecs_parse_json_desc_t *desc)
{
    char token[ECS_MAX_TOKEN_SIZE];
    int depth = 0;

    const char *name = NULL;
    const char *expr = NULL;

    ptr = ecs_parse_fluff(ptr, NULL);

    ecs_meta_cursor_t cur = ecs_meta_cursor(world, type, data_out);
    if (cur.valid == false) {
        return NULL;
    }

    if (desc) {
        name = desc->name;
        expr = desc->expr;
    }

    while ((ptr = ecs_parse_expr_token(name, expr, ptr, token))) {

        ptr = ecs_parse_fluff(ptr, NULL);

        if (!ecs_os_strcmp(token, "{")) {
            depth ++;
            if (ecs_meta_push(&cur) != 0) {
                goto error;
            }

            if (ecs_meta_is_collection(&cur)) {
                ecs_parser_error(name, expr, ptr - expr, "expected '['");
                return NULL;
            }
        }

        else if (!ecs_os_strcmp(token, "}")) {
            depth --;

            if (ecs_meta_is_collection(&cur)) {
                ecs_parser_error(name, expr, ptr - expr, "expected ']'");
                return NULL;
            }

            if (ecs_meta_pop(&cur) != 0) {
                goto error;
            }
        }

        else if (!ecs_os_strcmp(token, "[")) {
            depth ++;
            if (ecs_meta_push(&cur) != 0) {
                goto error;
            }

            if (!ecs_meta_is_collection(&cur)) {
                ecs_parser_error(name, expr, ptr - expr, "expected '{'");
                return NULL;
            }
        }

        else if (!ecs_os_strcmp(token, "]")) {
            depth --;

            if (!ecs_meta_is_collection(&cur)) {
                ecs_parser_error(name, expr, ptr - expr, "expected '}'");
                return NULL;
            }

            if (ecs_meta_pop(&cur) != 0) {
                goto error;
            }
        }

        else if (!ecs_os_strcmp(token, ",")) {
            if (ecs_meta_next(&cur) != 0) {
                goto error;
            }
        }

        else if (!ecs_os_strcmp(token, "null")) {
            if (ecs_meta_set_null(&cur) != 0) {
                goto error;
            }
        }

        else if (token[0] == '\"') {
            if (ptr[0] == ':') {
                /* Member assignment */
                ptr ++;

                /* Strip trailing " */
                ecs_size_t len = ecs_os_strlen(token);
                if (token[len - 1] != '"') {
                    ecs_parser_error(name, expr, ptr - expr, "expected \"");
                    return NULL;
                } else {
                    token[len - 1] = '\0';
                }

                if (ecs_meta_member(&cur, token + 1) != 0) {
                    goto error;
                }
            } else {
                if (ecs_meta_set_string_literal(&cur, token) != 0) {
                    goto error;
                }
            }
        }

        else {
            if (ecs_meta_set_string(&cur, token) != 0) {
                goto error;
            }
        }

        if (!depth) {
            break;
        }
    }

    return ptr;
error:
    return NULL;
}

#endif


#ifdef FLECS_REST

typedef struct {
    ecs_world_t *world;
    ecs_entity_t entity;
    ecs_http_server_t *srv;
    int32_t rc;
} ecs_rest_ctx_t;

static ECS_COPY(EcsRest, dst, src, {
    ecs_rest_ctx_t *impl = src->impl;
    if (impl) {
        impl->rc ++;
    }

    ecs_os_strset(&dst->ipaddr, src->ipaddr);
    dst->port = src->port;
    dst->impl = impl;
})

static ECS_MOVE(EcsRest, dst, src, {
    *dst = *src;
    src->ipaddr = NULL;
    src->impl = NULL;
})

static ECS_DTOR(EcsRest, ptr, { 
    ecs_rest_ctx_t *impl = ptr->impl;
    if (impl) {
        impl->rc --;
        if (!impl->rc) {
            ecs_http_server_fini(impl->srv);
            ecs_os_free(impl);
        }
    }
    ecs_os_free(ptr->ipaddr);
})

static char *rest_last_err;

static 
void flecs_rest_capture_log(
    int32_t level, 
    const char *file, 
    int32_t line, 
    const char *msg)
{
    (void)file; (void)line;

    if (!rest_last_err && level < 0) {
        rest_last_err = ecs_os_strdup(msg);
    }
}

static
char* flecs_rest_get_captured_log(void) {
    char *result = rest_last_err;
    rest_last_err = NULL;
    return result;
}

static
void flecs_reply_verror(
    ecs_http_reply_t *reply,
    const char *fmt,
    va_list args)
{
    ecs_strbuf_appendstr(&reply->body, "{\"error\":\"");
    ecs_strbuf_vappend(&reply->body, fmt, args);
    ecs_strbuf_appendstr(&reply->body, "\"}");
}

static
void flecs_reply_error(
    ecs_http_reply_t *reply,
    const char *fmt,
    ...)
{
    va_list args;
    va_start(args, fmt);
    flecs_reply_verror(reply, fmt, args);
    va_end(args);
}

static
void flecs_rest_bool_param(
    const ecs_http_request_t *req,
    const char *name,
    bool *value_out)
{
    const char *value = ecs_http_get_param(req, name);
    if (value) {
        if (!ecs_os_strcmp(value, "true")) {
            value_out[0] = true;
        } else {
            value_out[0] = false;
        }
    }
}

static
void flecs_rest_int_param(
    const ecs_http_request_t *req,
    const char *name,
    int32_t *value_out)
{
    const char *value = ecs_http_get_param(req, name);
    if (value) {
        *value_out = atoi(value);
    }
}

static
void flecs_rest_string_param(
    const ecs_http_request_t *req,
    const char *name,
    char **value_out)
{
    const char *value = ecs_http_get_param(req, name);
    if (value) {
        *value_out = (char*)value;
    }
}

static
void flecs_rest_parse_json_ser_entity_params(
    ecs_entity_to_json_desc_t *desc,
    const ecs_http_request_t *req)
{
    flecs_rest_bool_param(req, "path", &desc->serialize_path);
    flecs_rest_bool_param(req, "label", &desc->serialize_label);
    flecs_rest_bool_param(req, "brief", &desc->serialize_brief);
    flecs_rest_bool_param(req, "link", &desc->serialize_link);
    flecs_rest_bool_param(req, "color", &desc->serialize_color);
    flecs_rest_bool_param(req, "id_labels", &desc->serialize_id_labels);
    flecs_rest_bool_param(req, "base", &desc->serialize_base);
    flecs_rest_bool_param(req, "values", &desc->serialize_values);
    flecs_rest_bool_param(req, "private", &desc->serialize_private);
    flecs_rest_bool_param(req, "type_info", &desc->serialize_type_info);
}

static
void flecs_rest_parse_json_ser_iter_params(
    ecs_iter_to_json_desc_t *desc,
    const ecs_http_request_t *req)
{
    flecs_rest_bool_param(req, "term_ids", &desc->serialize_term_ids);
    flecs_rest_bool_param(req, "ids", &desc->serialize_ids);
    flecs_rest_bool_param(req, "sources", &desc->serialize_sources);
    flecs_rest_bool_param(req, "variables", &desc->serialize_variables);
    flecs_rest_bool_param(req, "is_set", &desc->serialize_is_set);
    flecs_rest_bool_param(req, "values", &desc->serialize_values);
    flecs_rest_bool_param(req, "entities", &desc->serialize_entities);
    flecs_rest_bool_param(req, "entity_labels", &desc->serialize_entity_labels);
    flecs_rest_bool_param(req, "entity_ids", &desc->serialize_entity_ids);
    flecs_rest_bool_param(req, "variable_labels", &desc->serialize_variable_labels);
    flecs_rest_bool_param(req, "variable_ids", &desc->serialize_variable_ids);
    flecs_rest_bool_param(req, "colors", &desc->serialize_colors);
    flecs_rest_bool_param(req, "duration", &desc->measure_eval_duration);
    flecs_rest_bool_param(req, "type_info", &desc->serialize_type_info);
}

static
bool flecs_rest_reply_entity(
    ecs_world_t *world,
    const ecs_http_request_t* req,
    ecs_http_reply_t *reply)
{
    char *path = &req->path[7];
    ecs_dbg_2("rest: request entity '%s'", path);

    ecs_entity_t e = ecs_lookup_path_w_sep(
        world, 0, path, "/", NULL, false);
    if (!e) {
        ecs_dbg_2("rest: entity '%s' not found", path);
        flecs_reply_error(reply, "entity '%s' not found", path);
        reply->code = 404;
        return true;
    }

    ecs_entity_to_json_desc_t desc = ECS_ENTITY_TO_JSON_INIT;
    flecs_rest_parse_json_ser_entity_params(&desc, req);

    ecs_entity_to_json_buf(world, e, &reply->body, &desc);
    return true;
}

static
bool flecs_rest_reply_query(
    ecs_world_t *world,
    const ecs_http_request_t* req,
    ecs_http_reply_t *reply)
{
    const char *q = ecs_http_get_param(req, "q");
    if (!q) {
        ecs_strbuf_appendstr(&reply->body, "Missing parameter 'q'");
        reply->code = 400; /* bad request */
        return true;
    }

    ecs_dbg_2("rest: request query '%s'", q);
    bool prev_color = ecs_log_enable_colors(false);
    ecs_os_api_log_t prev_log_ = ecs_os_api.log_;
    ecs_os_api.log_ = flecs_rest_capture_log;

    ecs_rule_t *r = ecs_rule_init(world, &(ecs_filter_desc_t){
        .expr = q
    });
    if (!r) {
        char *err = flecs_rest_get_captured_log();
        char *escaped_err = ecs_astresc('"', err);
        flecs_reply_error(reply, escaped_err);
        reply->code = 400; /* bad request */
        ecs_os_free(escaped_err);
        ecs_os_free(err);
    } else {
        ecs_iter_to_json_desc_t desc = ECS_ITER_TO_JSON_INIT;
        flecs_rest_parse_json_ser_iter_params(&desc, req);

        int32_t offset = 0;
        int32_t limit = 1000;

        flecs_rest_int_param(req, "offset", &offset);
        flecs_rest_int_param(req, "limit", &limit);

        ecs_iter_t it = ecs_rule_iter(world, r);
        ecs_iter_t pit = ecs_page_iter(&it, offset, limit);
        ecs_iter_to_json_buf(world, &pit, &reply->body, &desc);
        ecs_rule_fini(r);
    }

    ecs_os_api.log_ = prev_log_;
    ecs_log_enable_colors(prev_color);

    return true;
}

#ifdef FLECS_MONITOR

static
void flecs_rest_array_append(
    ecs_strbuf_t *reply,
    const char *field,
    const ecs_float_t *values,
    int32_t t)
{
    ecs_strbuf_list_append(reply, "\"%s\"", field);
    ecs_strbuf_appendch(reply, ':');
    ecs_strbuf_list_push(reply, "[", ",");

    int32_t i;
    for (i = t + 1; i <= (t + ECS_STAT_WINDOW); i ++) {
        int32_t index = i % ECS_STAT_WINDOW;
        ecs_strbuf_list_next(reply);
        ecs_strbuf_appendflt(reply, (double)values[index], '"');
    }

    ecs_strbuf_list_pop(reply, "]");
}

static
void flecs_rest_gauge_append(
    ecs_strbuf_t *reply,
    const ecs_metric_t *m,
    const char *field,
    int32_t t)
{
    ecs_strbuf_list_append(reply, "\"%s\"", field);
    ecs_strbuf_appendch(reply, ':');
    ecs_strbuf_list_push(reply, "{", ",");

    flecs_rest_array_append(reply, "avg", m->gauge.avg, t);
    flecs_rest_array_append(reply, "min", m->gauge.min, t);
    flecs_rest_array_append(reply, "max", m->gauge.max, t);

    ecs_strbuf_list_pop(reply, "}");
}

static
void flecs_rest_counter_append(
    ecs_strbuf_t *reply,
    const ecs_metric_t *m,
    const char *field,
    int32_t t)
{
    flecs_rest_gauge_append(reply, m, field, t);
}

#define ECS_GAUGE_APPEND_T(reply, s, field, t)\
    flecs_rest_gauge_append(reply, &(s)->field, #field, t)

#define ECS_COUNTER_APPEND_T(reply, s, field, t)\
    flecs_rest_counter_append(reply, &(s)->field, #field, t)

#define ECS_GAUGE_APPEND(reply, s, field)\
    ECS_GAUGE_APPEND_T(reply, s, field, (s)->t)

#define ECS_COUNTER_APPEND(reply, s, field)\
    ECS_COUNTER_APPEND_T(reply, s, field, (s)->t)

static
void flecs_world_stats_to_json(
    ecs_strbuf_t *reply,
    const EcsWorldStats *monitor_stats)
{
    const ecs_world_stats_t *stats = &monitor_stats->stats;

    ecs_strbuf_list_push(reply, "{", ",");
    ECS_GAUGE_APPEND(reply, stats, entity_count);
    ECS_GAUGE_APPEND(reply, stats, entity_not_alive_count);
    ECS_GAUGE_APPEND(reply, stats, id_count);
    ECS_GAUGE_APPEND(reply, stats, tag_id_count);
    ECS_GAUGE_APPEND(reply, stats, component_id_count);
    ECS_GAUGE_APPEND(reply, stats, pair_id_count);
    ECS_GAUGE_APPEND(reply, stats, wildcard_id_count);
    ECS_GAUGE_APPEND(reply, stats, component_count);
    ECS_COUNTER_APPEND(reply, stats, id_create_count);
    ECS_COUNTER_APPEND(reply, stats, id_delete_count);
    ECS_GAUGE_APPEND(reply, stats, table_count);
    ECS_GAUGE_APPEND(reply, stats, empty_table_count);
    ECS_GAUGE_APPEND(reply, stats, tag_table_count);
    ECS_GAUGE_APPEND(reply, stats, trivial_table_count);
    ECS_GAUGE_APPEND(reply, stats, table_record_count);
    ECS_GAUGE_APPEND(reply, stats, table_storage_count);
    ECS_COUNTER_APPEND(reply, stats, table_create_count);
    ECS_COUNTER_APPEND(reply, stats, table_delete_count);
    ECS_GAUGE_APPEND(reply, stats, query_count);
    ECS_GAUGE_APPEND(reply, stats, observer_count);
    ECS_GAUGE_APPEND(reply, stats, system_count);
    ECS_COUNTER_APPEND(reply, stats, new_count);
    ECS_COUNTER_APPEND(reply, stats, bulk_new_count);
    ECS_COUNTER_APPEND(reply, stats, delete_count);
    ECS_COUNTER_APPEND(reply, stats, clear_count);
    ECS_COUNTER_APPEND(reply, stats, add_count);
    ECS_COUNTER_APPEND(reply, stats, remove_count);
    ECS_COUNTER_APPEND(reply, stats, set_count);
    ECS_COUNTER_APPEND(reply, stats, discard_count);
    ECS_COUNTER_APPEND(reply, stats, world_time_total_raw);
    ECS_COUNTER_APPEND(reply, stats, world_time_total);
    ECS_COUNTER_APPEND(reply, stats, frame_time_total);
    ECS_COUNTER_APPEND(reply, stats, system_time_total);
    ECS_COUNTER_APPEND(reply, stats, merge_time_total);
    ECS_GAUGE_APPEND(reply, stats, fps);
    ECS_GAUGE_APPEND(reply, stats, delta_time);
    ECS_COUNTER_APPEND(reply, stats, frame_count_total);
    ECS_COUNTER_APPEND(reply, stats, merge_count_total);
    ECS_COUNTER_APPEND(reply, stats, pipeline_build_count_total);
    ECS_COUNTER_APPEND(reply, stats, systems_ran_frame);
    ECS_COUNTER_APPEND(reply, stats, alloc_count);
    ECS_COUNTER_APPEND(reply, stats, realloc_count);
    ECS_COUNTER_APPEND(reply, stats, free_count);
    ECS_GAUGE_APPEND(reply, stats, outstanding_alloc_count);
    ecs_strbuf_list_pop(reply, "}");
}

static
void flecs_system_stats_to_json(
    ecs_world_t *world,
    ecs_strbuf_t *reply,
    ecs_entity_t system,
    const ecs_system_stats_t *stats)
{
    ecs_strbuf_list_push(reply, "{", ",");

    char *path = ecs_get_fullpath(world, system);
    ecs_strbuf_list_append(reply, "\"name\":\"%s\"", path);
    ecs_os_free(path);

    if (!stats->task) {
        ECS_GAUGE_APPEND(reply, &stats->query, matched_table_count);
        ECS_GAUGE_APPEND(reply, &stats->query, matched_entity_count);
    }

    ECS_COUNTER_APPEND_T(reply, stats, time_spent, stats->query.t);
    ecs_strbuf_list_pop(reply, "}");
}

static
void flecs_pipeline_stats_to_json(
    ecs_world_t *world,
    ecs_strbuf_t *reply,
    const EcsPipelineStats *stats)
{
    ecs_strbuf_list_push(reply, "[", ",");

    int32_t i, count = ecs_vector_count(stats->stats.systems);
    ecs_entity_t *ids = ecs_vector_first(stats->stats.systems, ecs_entity_t);
    for (i = 0; i < count; i ++) {
        ecs_entity_t id = ids[i];
        
        ecs_strbuf_list_next(reply);

        if (id) {
            ecs_system_stats_t *sys_stats = ecs_map_get(
                    &stats->stats.system_stats, ecs_system_stats_t, id);
            flecs_system_stats_to_json(world, reply, id, sys_stats);
        } else {
            /* Sync point */
            ecs_strbuf_list_push(reply, "{", ",");
            ecs_strbuf_list_pop(reply, "}");
        }
    }

    ecs_strbuf_list_pop(reply, "]");
}

static
bool flecs_rest_reply_stats(
    ecs_world_t *world,
    const ecs_http_request_t* req,
    ecs_http_reply_t *reply)
{
    char *period_str = NULL;
    flecs_rest_string_param(req, "period", &period_str);
    char *category = &req->path[6];

    ecs_entity_t period = EcsPeriod1s;
    if (period_str) {
        char *period_name = ecs_asprintf("Period%s", period_str);
        period = ecs_lookup_child(world, ecs_id(FlecsMonitor), period_name);
        ecs_os_free(period_name);
        if (!period) {
            flecs_reply_error(reply, "bad request (invalid period string)");
            reply->code = 400;
            return false;
        }
    }

    if (!ecs_os_strcmp(category, "world")) {
        const EcsWorldStats *stats = ecs_get_pair(world, EcsWorld, 
            EcsWorldStats, period);
        flecs_world_stats_to_json(&reply->body, stats);
        return true;

    } else if (!ecs_os_strcmp(category, "pipeline")) {
        const EcsPipelineStats *stats = ecs_get_pair(world, EcsWorld, 
            EcsPipelineStats, period);
        flecs_pipeline_stats_to_json(world, &reply->body, stats);
        return true;

    } else {
        flecs_reply_error(reply, "bad request (unsupported category)");
        reply->code = 400;
        return false;
    }

    return true;
}
#else
static
bool flecs_rest_reply_stats(
    ecs_world_t *world,
    const ecs_http_request_t* req,
    ecs_http_reply_t *reply)
{
    (void)world;
    (void)req;
    (void)reply;
    return false;
}
#endif

static
void flecs_rest_reply_table_append_type(
    ecs_world_t *world,
    ecs_strbuf_t *reply,
    const ecs_table_t *table)
{
    ecs_strbuf_list_push(reply, "[", ",");
    int32_t i, count = table->type.count;
    ecs_id_t *ids = table->type.array;
    for (i = 0; i < count; i ++) {
        ecs_strbuf_list_next(reply);
        ecs_strbuf_appendch(reply, '"');
        ecs_id_str_buf(world, ids[i], reply);
        ecs_strbuf_appendch(reply, '"');
    }
    ecs_strbuf_list_pop(reply, "]");
}

static
void flecs_rest_reply_table_append_memory(
    ecs_strbuf_t *reply,
    const ecs_table_t *table)
{
    int32_t used = 0, allocated = 0;

    used += table->data.entities.count * ECS_SIZEOF(ecs_entity_t);
    used += table->data.records.count * ECS_SIZEOF(ecs_record_t*);
    allocated += table->data.entities.size * ECS_SIZEOF(ecs_entity_t);
    allocated += table->data.records.size * ECS_SIZEOF(ecs_record_t*);

    int32_t i, storage_count = table->storage_count;
    ecs_type_info_t **ti = table->type_info;
    ecs_column_t *storages = table->data.columns;

    for (i = 0; i < storage_count; i ++) {
        used += storages[i].count * ti[i]->size;
        allocated += storages[i].size * ti[i]->size;
    }

    ecs_strbuf_list_push(reply, "{", ",");
    ecs_strbuf_list_append(reply, "\"used\":%d", used);
    ecs_strbuf_list_append(reply, "\"allocated\":%d", allocated);
    ecs_strbuf_list_pop(reply, "}");
}

static
void flecs_rest_reply_table_append(
    ecs_world_t *world,
    ecs_strbuf_t *reply,
    const ecs_table_t *table)
{
    ecs_strbuf_list_next(reply);
    ecs_strbuf_list_push(reply, "{", ",");
    ecs_strbuf_list_append(reply, "\"id\":%u", (uint32_t)table->id);
    ecs_strbuf_list_appendstr(reply, "\"type\":");
    flecs_rest_reply_table_append_type(world, reply, table);
    ecs_strbuf_list_append(reply, "\"count\":%d", ecs_table_count(table));
    ecs_strbuf_list_append(reply, "\"memory\":");
    flecs_rest_reply_table_append_memory(reply, table);
    ecs_strbuf_list_append(reply, "\"refcount\":%d", table->refcount);
    ecs_strbuf_list_pop(reply, "}");
}

static
bool flecs_rest_reply_tables(
    ecs_world_t *world,
    const ecs_http_request_t* req,
    ecs_http_reply_t *reply)
{
    (void)req;

    ecs_strbuf_list_push(&reply->body, "[", ",");
    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(tables, ecs_table_t, i);
        flecs_rest_reply_table_append(world, &reply->body, table);
    }
    ecs_strbuf_list_pop(&reply->body, "]");

    return true;
}

static
void flecs_rest_reply_id_append(
    ecs_world_t *world,
    ecs_strbuf_t *reply,
    const ecs_id_record_t *idr)
{
    ecs_strbuf_list_next(reply);
    ecs_strbuf_list_push(reply, "{", ",");
    ecs_strbuf_list_appendstr(reply, "\"id\":\"");
    ecs_id_str_buf(world, idr->id, reply);
    ecs_strbuf_appendch(reply, '"');

    if (idr->type_info) {
        if (idr->type_info->component != idr->id) {
            ecs_strbuf_list_appendstr(reply, "\"component\":\"");
            ecs_id_str_buf(world, idr->type_info->component, reply);
            ecs_strbuf_appendch(reply, '"');
        }

        ecs_strbuf_list_append(reply, "\"size\":%d", 
            idr->type_info->size);
        ecs_strbuf_list_append(reply, "\"alignment\":%d", 
            idr->type_info->alignment);
    }

    ecs_strbuf_list_append(reply, "\"table_count\":%d", 
        idr->cache.tables.count);
    ecs_strbuf_list_append(reply, "\"empty_table_count\":%d", 
        idr->cache.empty_tables.count);

    ecs_strbuf_list_pop(reply, "}");
}

static
bool flecs_rest_reply_ids(
    ecs_world_t *world,
    const ecs_http_request_t* req,
    ecs_http_reply_t *reply)
{
    (void)req;

    ecs_strbuf_list_push(&reply->body, "[", ",");
    ecs_map_iter_t it = ecs_map_iter(&world->id_index);
    ecs_id_record_t *idr;
    while ((idr = ecs_map_next_ptr(&it, ecs_id_record_t*, NULL))) {
        flecs_rest_reply_id_append(world, &reply->body, idr);
    }
    ecs_strbuf_list_pop(&reply->body, "]");

    return true;
}

static
bool flecs_rest_reply(
    const ecs_http_request_t* req,
    ecs_http_reply_t *reply,
    void *ctx)
{
    ecs_rest_ctx_t *impl = ctx;
    ecs_world_t *world = impl->world;

    if (req->path == NULL) {
        ecs_dbg("rest: bad request (missing path)");
        flecs_reply_error(reply, "bad request (missing path)");
        reply->code = 400;
        return false;
    }

    ecs_strbuf_appendstr(&reply->headers, "Access-Control-Allow-Origin: *\r\n");

    if (req->method == EcsHttpGet) {
        /* Entity endpoint */
        if (!ecs_os_strncmp(req->path, "entity/", 7)) {
            return flecs_rest_reply_entity(world, req, reply);
        
        /* Query endpoint */
        } else if (!ecs_os_strcmp(req->path, "query")) {
            return flecs_rest_reply_query(world, req, reply);

        /* Stats endpoint */
        } else if (!ecs_os_strncmp(req->path, "stats/", 6)) {
            return flecs_rest_reply_stats(world, req, reply);

        /* Tables endpoint */
        } else if (!ecs_os_strncmp(req->path, "tables", 6)) {
            return flecs_rest_reply_tables(world, req, reply);

        /* Ids endpoint */
        } else if (!ecs_os_strncmp(req->path, "ids", 3)) {
            return flecs_rest_reply_ids(world, req, reply);
        }
    } else if (req->method == EcsHttpOptions) {
        return true;
    }

    return false;
}

static
void flecs_on_set_rest(ecs_iter_t *it)
{
    EcsRest *rest = it->ptrs[0];

    int i;
    for(i = 0; i < it->count; i ++) {
        if (!rest[i].port) {
            rest[i].port = ECS_REST_DEFAULT_PORT;
        }

        ecs_rest_ctx_t *srv_ctx = ecs_os_malloc_t(ecs_rest_ctx_t);
        ecs_http_server_t *srv = ecs_http_server_init(&(ecs_http_server_desc_t){
            .ipaddr = rest[i].ipaddr,
            .port = rest[i].port,
            .callback = flecs_rest_reply,
            .ctx = srv_ctx
        });

        if (!srv) {
            const char *ipaddr = rest[i].ipaddr ? rest[i].ipaddr : "0.0.0.0";
            ecs_err("failed to create REST server on %s:%u", 
                ipaddr, rest[i].port);
            ecs_os_free(srv_ctx);
            continue;
        }

        srv_ctx->world = it->world;
        srv_ctx->entity = it->entities[i];
        srv_ctx->srv = srv;
        srv_ctx->rc = 1;

        rest[i].impl = srv_ctx;

        ecs_http_server_start(srv_ctx->srv);
    }
}

static
void DequeueRest(ecs_iter_t *it) {
    EcsRest *rest = ecs_field(it, EcsRest, 1);

    if (it->delta_system_time > (ecs_ftime_t)1.0) {
        ecs_warn(
            "detected large progress interval (%.2fs), REST request may timeout",
            (double)it->delta_system_time);
    }

    int32_t i;
    for(i = 0; i < it->count; i ++) {
        ecs_rest_ctx_t *ctx = rest[i].impl;
        if (ctx) {
            ecs_http_server_dequeue(ctx->srv, it->delta_time);
        }
    } 
}

void FlecsRestImport(
    ecs_world_t *world)
{
    ECS_MODULE(world, FlecsRest);

    ecs_set_name_prefix(world, "Ecs");

    flecs_bootstrap_component(world, EcsRest);

    ecs_set_hooks(world, EcsRest, { 
        .ctor = ecs_default_ctor,
        .move = ecs_move(EcsRest),
        .copy = ecs_copy(EcsRest),
        .dtor = ecs_dtor(EcsRest),
        .on_set = flecs_on_set_rest
    });

    ECS_SYSTEM(world, DequeueRest, EcsPostFrame, EcsRest);
}

#endif



#ifdef FLECS_COREDOC

#define URL_ROOT "https://flecs.docsforge.com/master/relationships-manual/"

void FlecsCoreDocImport(
    ecs_world_t *world)
{    
    ECS_MODULE(world, FlecsCoreDoc);

    ECS_IMPORT(world, FlecsMeta);
    ECS_IMPORT(world, FlecsDoc);

    ecs_set_name_prefix(world, "Ecs");

    /* Initialize reflection data for core components */

    ecs_struct_init(world, &(ecs_struct_desc_t){
        .entity = ecs_id(EcsComponent),
        .members = {
            {.name = (char*)"size", .type = ecs_id(ecs_i32_t)},
            {.name = (char*)"alignment", .type = ecs_id(ecs_i32_t)}
        }
    });

    ecs_struct_init(world, &(ecs_struct_desc_t){
        .entity = ecs_id(EcsDocDescription),
        .members = {
            {.name = "value", .type = ecs_id(ecs_string_t)}
        }
    });

    /* Initialize documentation data for core components */
    ecs_doc_set_brief(world, EcsFlecs, "Flecs root module");
    ecs_doc_set_link(world, EcsFlecs, "https://github.com/SanderMertens/flecs");

    ecs_doc_set_brief(world, EcsFlecsCore, "Flecs module with builtin components");

    ecs_doc_set_brief(world, EcsWorld, "Entity associated with world");

    ecs_doc_set_brief(world, ecs_id(EcsComponent), "Component that is added to all components");
    ecs_doc_set_brief(world, EcsModule, "Tag that is added to modules");
    ecs_doc_set_brief(world, EcsPrefab, "Tag that is added to prefabs");
    ecs_doc_set_brief(world, EcsDisabled, "Tag that is added to disabled entities");

    ecs_doc_set_brief(world, ecs_id(EcsIdentifier), "Component used for entity names");
    ecs_doc_set_brief(world, EcsName, "Tag used with EcsIdentifier to signal entity name");
    ecs_doc_set_brief(world, EcsSymbol, "Tag used with EcsIdentifier to signal entity symbol");

    ecs_doc_set_brief(world, EcsTransitive, "Transitive relationship property");
    ecs_doc_set_brief(world, EcsReflexive, "Reflexive relationship property");
    ecs_doc_set_brief(world, EcsFinal, "Final relationship property");
    ecs_doc_set_brief(world, EcsDontInherit, "DontInherit relationship property");
    ecs_doc_set_brief(world, EcsTag, "Tag relationship property");
    ecs_doc_set_brief(world, EcsAcyclic, "Acyclic relationship property");
    ecs_doc_set_brief(world, EcsExclusive, "Exclusive relationship property");
    ecs_doc_set_brief(world, EcsSymmetric, "Symmetric relationship property");
    ecs_doc_set_brief(world, EcsWith, "With relationship property");
    ecs_doc_set_brief(world, EcsOnDelete, "OnDelete relationship cleanup property");
    ecs_doc_set_brief(world, EcsOnDeleteTarget, "OnDeleteTarget relationship cleanup property");
    ecs_doc_set_brief(world, EcsDefaultChildComponent, "Sets default component hint for children of entity");
    ecs_doc_set_brief(world, EcsRemove, "Remove relationship cleanup property");
    ecs_doc_set_brief(world, EcsDelete, "Delete relationship cleanup property");
    ecs_doc_set_brief(world, EcsPanic, "Panic relationship cleanup property");
    ecs_doc_set_brief(world, EcsIsA, "Builtin IsA relationship");
    ecs_doc_set_brief(world, EcsChildOf, "Builtin ChildOf relationship");
    ecs_doc_set_brief(world, EcsDependsOn, "Builtin DependsOn relationship");
    ecs_doc_set_brief(world, EcsOnAdd, "Builtin OnAdd event");
    ecs_doc_set_brief(world, EcsOnRemove, "Builtin OnRemove event");
    ecs_doc_set_brief(world, EcsOnSet, "Builtin OnSet event");
    ecs_doc_set_brief(world, EcsUnSet, "Builtin UnSet event");

    ecs_doc_set_link(world, EcsTransitive, URL_ROOT "#transitive-property");
    ecs_doc_set_link(world, EcsReflexive, URL_ROOT "#reflexive-property");
    ecs_doc_set_link(world, EcsFinal, URL_ROOT "#final-property");
    ecs_doc_set_link(world, EcsDontInherit, URL_ROOT "#dontinherit-property");
    ecs_doc_set_link(world, EcsTag, URL_ROOT "#tag-property");
    ecs_doc_set_link(world, EcsAcyclic, URL_ROOT "#acyclic-property");
    ecs_doc_set_link(world, EcsExclusive, URL_ROOT "#exclusive-property");
    ecs_doc_set_link(world, EcsSymmetric, URL_ROOT "#symmetric-property");
    ecs_doc_set_link(world, EcsWith, URL_ROOT "#with-property");
    ecs_doc_set_link(world, EcsOnDelete, URL_ROOT "#cleanup-properties");
    ecs_doc_set_link(world, EcsOnDeleteTarget, URL_ROOT "#cleanup-properties");
    ecs_doc_set_link(world, EcsRemove, URL_ROOT "#cleanup-properties");
    ecs_doc_set_link(world, EcsDelete, URL_ROOT "#cleanup-properties");
    ecs_doc_set_link(world, EcsPanic, URL_ROOT "#cleanup-properties");
    ecs_doc_set_link(world, EcsIsA, URL_ROOT "#the-isa-relationship");
    ecs_doc_set_link(world, EcsChildOf, URL_ROOT "#the-childof-relationship"); 
    
    /* Initialize documentation for meta components */
    ecs_entity_t meta = ecs_lookup_fullpath(world, "flecs.meta");
    ecs_doc_set_brief(world, meta, "Flecs module with reflection components");

    ecs_doc_set_brief(world, ecs_id(EcsMetaType), "Component added to types");
    ecs_doc_set_brief(world, ecs_id(EcsMetaTypeSerialized), "Component that stores reflection data in an optimized format");
    ecs_doc_set_brief(world, ecs_id(EcsPrimitive), "Component added to primitive types");
    ecs_doc_set_brief(world, ecs_id(EcsEnum), "Component added to enumeration types");
    ecs_doc_set_brief(world, ecs_id(EcsBitmask), "Component added to bitmask types");
    ecs_doc_set_brief(world, ecs_id(EcsMember), "Component added to struct members");
    ecs_doc_set_brief(world, ecs_id(EcsStruct), "Component added to struct types");
    ecs_doc_set_brief(world, ecs_id(EcsArray), "Component added to array types");
    ecs_doc_set_brief(world, ecs_id(EcsVector), "Component added to vector types");

    ecs_doc_set_brief(world, ecs_id(ecs_bool_t), "bool component");
    ecs_doc_set_brief(world, ecs_id(ecs_char_t), "char component");
    ecs_doc_set_brief(world, ecs_id(ecs_byte_t), "byte component");
    ecs_doc_set_brief(world, ecs_id(ecs_u8_t), "8 bit unsigned int component");
    ecs_doc_set_brief(world, ecs_id(ecs_u16_t), "16 bit unsigned int component");
    ecs_doc_set_brief(world, ecs_id(ecs_u32_t), "32 bit unsigned int component");
    ecs_doc_set_brief(world, ecs_id(ecs_u64_t), "64 bit unsigned int component");
    ecs_doc_set_brief(world, ecs_id(ecs_uptr_t), "word sized unsigned int component");
    ecs_doc_set_brief(world, ecs_id(ecs_i8_t), "8 bit signed int component");
    ecs_doc_set_brief(world, ecs_id(ecs_i16_t), "16 bit signed int component");
    ecs_doc_set_brief(world, ecs_id(ecs_i32_t), "32 bit signed int component");
    ecs_doc_set_brief(world, ecs_id(ecs_i64_t), "64 bit signed int component");
    ecs_doc_set_brief(world, ecs_id(ecs_iptr_t), "word sized signed int component");
    ecs_doc_set_brief(world, ecs_id(ecs_f32_t), "32 bit floating point component");
    ecs_doc_set_brief(world, ecs_id(ecs_f64_t), "64 bit floating point component");
    ecs_doc_set_brief(world, ecs_id(ecs_string_t), "string component");
    ecs_doc_set_brief(world, ecs_id(ecs_entity_t), "entity component");

    /* Initialize documentation for doc components */
    ecs_entity_t doc = ecs_lookup_fullpath(world, "flecs.doc");
    ecs_doc_set_brief(world, doc, "Flecs module with documentation components");

    ecs_doc_set_brief(world, ecs_id(EcsDocDescription), "Component used to add documentation");
    ecs_doc_set_brief(world, EcsDocBrief, "Used as (Description, Brief) to add a brief description");
    ecs_doc_set_brief(world, EcsDocDetail, "Used as (Description, Detail) to add a detailed description");
    ecs_doc_set_brief(world, EcsDocLink, "Used as (Description, Link) to add a link");
}

#endif

/* This is a heavily modified version of the EmbeddableWebServer (see copyright
 * below). This version has been stripped from everything not strictly necessary
 * for receiving/replying to simple HTTP requests, and has been modified to use
 * the Flecs OS API. */

/* EmbeddableWebServer Copyright (c) 2016, 2019, 2020 Forrest Heller, and 
 * CONTRIBUTORS (see below) - All rights reserved.
 *
 * CONTRIBUTORS:
 * Martin Pulec - bug fixes, warning fixes, IPv6 support
 * Daniel Barry - bug fix (ifa_addr != NULL)
 * 
 * Released under the BSD 2-clause license:
 * Redistribution and use in source and binary forms, with or without 
 * modification, are permitted provided that the following conditions are met:
 * 1. Redistributions of source code must retain the above copyright notice, 
 * this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright notice, 
 * this list of conditions and the following disclaimer in the documentation 
 * and/or other materials provided with the distribution. THIS SOFTWARE IS 
 * PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS 
 * OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES 
 * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN 
 * NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY 
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF 
 * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
 */


#ifdef FLECS_HTTP

#if defined(ECS_TARGET_WINDOWS)
#ifndef WIN32_LEAN_AND_MEAN
#define WIN32_LEAN_AND_MEAN
#endif
#pragma comment(lib, "Ws2_32.lib")
#include <WinSock2.h>
#include <Ws2tcpip.h>
#include <Windows.h>
typedef SOCKET ecs_http_socket_t;
#else
#include <unistd.h>
#include <arpa/inet.h>
#include <sys/socket.h>
#include <netdb.h>
#include <strings.h>
#include <signal.h>
typedef int ecs_http_socket_t;
#endif

/* Max length of request method */
#define ECS_HTTP_METHOD_LEN_MAX (8) 

/* Timeout (s) before connection purge */
#define ECS_HTTP_CONNECTION_PURGE_TIMEOUT (1.0)

/* Number of dequeues before purging */
#define ECS_HTTP_CONNECTION_PURGE_RETRY_COUNT (5)

/* Minimum interval between dequeueing requests (ms) */
#define ECS_HTTP_MIN_DEQUEUE_INTERVAL (100)

/* Minimum interval between printing statistics (ms) */
#define ECS_HTTP_MIN_STATS_INTERVAL (10 * 1000)

/* Max length of headers in reply */
#define ECS_HTTP_REPLY_HEADER_SIZE (1024)

/* Receive buffer size */
#define ECS_HTTP_SEND_RECV_BUFFER_SIZE (16 * 1024)

/* Max length of request (path + query + headers + body) */
#define ECS_HTTP_REQUEST_LEN_MAX (10 * 1024 * 1024)

/* HTTP server struct */
struct ecs_http_server_t {
    bool should_run;
    bool running;

    ecs_http_socket_t sock;
    ecs_os_mutex_t lock;
    ecs_os_thread_t thread;

    ecs_http_reply_action_t callback;
    void *ctx;

    ecs_sparse_t *connections; /* sparse<http_connection_t> */
    ecs_sparse_t *requests; /* sparse<http_request_t> */

    bool initialized;

    uint16_t port;
    const char *ipaddr;

    ecs_ftime_t dequeue_timeout; /* used to not lock request queue too often */
    ecs_ftime_t stats_timeout; /* used for periodic reporting of statistics */

    ecs_ftime_t request_time; /* time spent on requests in last stats interval */
    ecs_ftime_t request_time_total; /* total time spent on requests */
    int32_t requests_processed; /* requests processed in last stats interval */
    int32_t requests_processed_total; /* total requests processed */
    int32_t dequeue_count; /* number of dequeues in last stats interval */
};

/** Fragment state, used by HTTP request parser */
typedef enum  {
    HttpFragStateBegin,
    HttpFragStateMethod,
    HttpFragStatePath,
    HttpFragStateVersion,
    HttpFragStateHeaderStart,
    HttpFragStateHeaderName,
    HttpFragStateHeaderValueStart,
    HttpFragStateHeaderValue,
    HttpFragStateCR,
    HttpFragStateCRLF,
    HttpFragStateCRLFCR,
    HttpFragStateBody,
    HttpFragStateDone
} HttpFragState;

/** A fragment is a partially received HTTP request */
typedef struct {
    HttpFragState state;
    ecs_strbuf_t buf;
    ecs_http_method_t method;
    int32_t body_offset;
    int32_t query_offset;
    int32_t header_offsets[ECS_HTTP_HEADER_COUNT_MAX];
    int32_t header_value_offsets[ECS_HTTP_HEADER_COUNT_MAX];
    int32_t header_count;
    int32_t param_offsets[ECS_HTTP_QUERY_PARAM_COUNT_MAX];
    int32_t param_value_offsets[ECS_HTTP_QUERY_PARAM_COUNT_MAX];
    int32_t param_count;
    char header_buf[32];
    char *header_buf_ptr;
    int32_t content_length;
    bool parse_content_length;
    bool invalid;
} ecs_http_fragment_t;

/** Extend public connection type with fragment data */
typedef struct {
    ecs_http_connection_t pub;
    ecs_http_fragment_t frag;
    ecs_http_socket_t sock;

    /* Connection is purged after both timeout expires and connection has
     * exceeded retry count. This ensures that a connection does not immediately
     * timeout when a frame takes longer than usual */
    ecs_ftime_t dequeue_timeout;
    int32_t dequeue_retries;    
} ecs_http_connection_impl_t;

typedef struct {
    ecs_http_request_t pub;
    uint64_t conn_id; /* for sanity check */
    void *res;
} ecs_http_request_impl_t;

static
ecs_size_t http_send(
    ecs_http_socket_t sock, 
    const void *buf, 
    ecs_size_t size, 
    int flags)
{
#ifndef ECS_TARGET_MSVC
    ssize_t send_bytes = send(sock, buf, flecs_itosize(size), flags);
    return flecs_itoi32(send_bytes);
#else
    int send_bytes = send(sock, buf, size, flags);
    return flecs_itoi32(send_bytes);
#endif
}

static
ecs_size_t http_recv(
    ecs_http_socket_t sock,
    void *buf,
    ecs_size_t size,
    int flags)
{
    ecs_size_t ret;
#ifndef ECS_TARGET_MSVC
    ssize_t recv_bytes = recv(sock, buf, flecs_itosize(size), flags);
    ret = flecs_itoi32(recv_bytes);
#else
    int recv_bytes = recv(sock, buf, size, flags);
    ret = flecs_itoi32(recv_bytes);
#endif
    if (ret == -1) {
        ecs_dbg("recv failed: %s (sock = %d)", ecs_os_strerror(errno), sock);
    } else if (ret == 0) {
        ecs_dbg("recv: received 0 bytes (sock = %d)", sock);
    }

    return ret;
}

static
int http_getnameinfo(
    const struct sockaddr* addr,
    ecs_size_t addr_len,
    char *host,
    ecs_size_t host_len,
    char *port,
    ecs_size_t port_len,
    int flags)
{
    ecs_assert(addr_len > 0, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(host_len > 0, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(port_len > 0, ECS_INTERNAL_ERROR, NULL);
    return getnameinfo(addr, (uint32_t)addr_len, host, (uint32_t)host_len, 
        port, (uint32_t)port_len, flags);
}

static
int http_bind(
    ecs_http_socket_t sock,
    const struct sockaddr* addr,
    ecs_size_t addr_len)
{
    ecs_assert(addr_len > 0, ECS_INTERNAL_ERROR, NULL);
    return bind(sock, addr, (uint32_t)addr_len);
}

static
bool http_socket_is_valid(
    ecs_http_socket_t sock)
{
#if defined(ECS_TARGET_WINDOWS)
    return sock != INVALID_SOCKET;
#else
    return sock >= 0;
#endif
}

#if defined(ECS_TARGET_WINDOWS)
#define HTTP_SOCKET_INVALID INVALID_SOCKET
#else
#define HTTP_SOCKET_INVALID (-1)
#endif

static
void http_close(
    ecs_http_socket_t *sock)
{
    ecs_assert(sock != NULL, ECS_INTERNAL_ERROR, NULL);
#if defined(ECS_TARGET_WINDOWS)
    closesocket(*sock);
#else
    ecs_dbg_2("http: closing socket %u", *sock);
    shutdown(*sock, SHUT_RDWR);
    close(*sock);
#endif
    *sock = HTTP_SOCKET_INVALID;
}

static
ecs_http_socket_t http_accept(
    ecs_http_socket_t sock,
    struct sockaddr* addr,
    ecs_size_t *addr_len)
{
    socklen_t len = (socklen_t)addr_len[0];
    ecs_http_socket_t result = accept(sock, addr, &len);
    addr_len[0] = (ecs_size_t)len;
    return result;
}

static 
void reply_free(ecs_http_reply_t* response) {
    ecs_assert(response != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_os_free(response->body.content);
}

static
void request_free(ecs_http_request_impl_t *req) {
    ecs_assert(req != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(req->pub.conn != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(req->pub.conn->server != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(req->pub.conn->server->requests != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(req->pub.conn->id == req->conn_id, ECS_INTERNAL_ERROR, NULL);
    ecs_os_free(req->res);
    flecs_sparse_remove(req->pub.conn->server->requests, req->pub.id);
}

static
void connection_free(ecs_http_connection_impl_t *conn) {
    ecs_assert(conn != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(conn->pub.id != 0, ECS_INTERNAL_ERROR, NULL);
    uint64_t conn_id = conn->pub.id;

    if (http_socket_is_valid(conn->sock)) {
        http_close(&conn->sock);
    }

    flecs_sparse_remove(conn->pub.server->connections, conn_id);
}

// https://stackoverflow.com/questions/10156409/convert-hex-string-char-to-int
static
char hex_2_int(char a, char b){
    a = (a <= '9') ? (char)(a - '0') : (char)((a & 0x7) + 9);
    b = (b <= '9') ? (char)(b - '0') : (char)((b & 0x7) + 9);
    return (char)((a << 4) + b);
}

static
void decode_url_str(
    char *str) 
{
    char ch, *ptr, *dst = str;
    for (ptr = str; (ch = *ptr); ptr++) {
        if (ch == '%') {
            dst[0] = hex_2_int(ptr[1], ptr[2]);
            dst ++;
            ptr += 2;
        } else {
            dst[0] = ptr[0];
            dst ++;
        }
    }
    dst[0] = '\0';
}

static
void parse_method(
    ecs_http_fragment_t *frag)
{
    char *method = ecs_strbuf_get_small(&frag->buf);
    if (!ecs_os_strcmp(method, "GET")) frag->method = EcsHttpGet;
    else if (!ecs_os_strcmp(method, "POST")) frag->method = EcsHttpPost;
    else if (!ecs_os_strcmp(method, "PUT")) frag->method = EcsHttpPut;
    else if (!ecs_os_strcmp(method, "DELETE")) frag->method = EcsHttpDelete;
    else if (!ecs_os_strcmp(method, "OPTIONS")) frag->method = EcsHttpOptions;
    else {
        frag->method = EcsHttpMethodUnsupported;
        frag->invalid = true;
    }
    ecs_strbuf_reset(&frag->buf);
}

static
bool header_writable(
    ecs_http_fragment_t *frag)
{
    return frag->header_count < ECS_HTTP_HEADER_COUNT_MAX;
}

static
void header_buf_reset(
    ecs_http_fragment_t *frag)
{
    frag->header_buf[0] = '\0';
    frag->header_buf_ptr = frag->header_buf;
}

static
void header_buf_append(
    ecs_http_fragment_t *frag,
    char ch)
{
    if ((frag->header_buf_ptr - frag->header_buf) < 
        ECS_SIZEOF(frag->header_buf)) 
    {
        frag->header_buf_ptr[0] = ch;
        frag->header_buf_ptr ++;
    } else {
        frag->header_buf_ptr[0] = '\0';
    }
}

static
void enqueue_request(
    ecs_http_connection_impl_t *conn)
{
    ecs_http_server_t *srv = conn->pub.server;
    ecs_http_fragment_t *frag = &conn->frag;

    if (frag->invalid) { /* invalid request received, don't enqueue */
        ecs_strbuf_reset(&frag->buf);
    } else {
        char *res = ecs_strbuf_get(&frag->buf);
        if (res) {
            ecs_os_mutex_lock(srv->lock);
            ecs_http_request_impl_t *req = flecs_sparse_add(
                srv->requests, ecs_http_request_impl_t);
            req->pub.id = flecs_sparse_last_id(srv->requests);
            req->conn_id = conn->pub.id;

            req->pub.conn = (ecs_http_connection_t*)conn;
            req->pub.method = frag->method;
            req->pub.path = res + 1;
            if (frag->body_offset) {
                req->pub.body = &res[frag->body_offset];
            }
            int32_t i, count = frag->header_count;
            for (i = 0; i < count; i ++) {
                req->pub.headers[i].key = &res[frag->header_offsets[i]];
                req->pub.headers[i].value = &res[frag->header_value_offsets[i]];
            }
            count = frag->param_count;
            for (i = 0; i < count; i ++) {
                req->pub.params[i].key = &res[frag->param_offsets[i]];
                req->pub.params[i].value = &res[frag->param_value_offsets[i]];
                decode_url_str((char*)req->pub.params[i].value);
            }

            req->pub.header_count = frag->header_count;
            req->pub.param_count = frag->param_count;
            req->res = res;
            ecs_os_mutex_unlock(srv->lock);
        }
    }
}

static
bool parse_request(
    ecs_http_connection_impl_t *conn,
    uint64_t conn_id,
    const char* req_frag, 
    ecs_size_t req_frag_len) 
{
    ecs_http_fragment_t *frag = &conn->frag;

    int32_t i;
    for (i = 0; i < req_frag_len; i++) {
        char c = req_frag[i];
        switch (frag->state) {
        case HttpFragStateBegin:
            ecs_os_memset_t(frag, 0, ecs_http_fragment_t);
            frag->buf.max = ECS_HTTP_METHOD_LEN_MAX;
            frag->state = HttpFragStateMethod;
            frag->header_buf_ptr = frag->header_buf;
            /* fallthrough */
        case HttpFragStateMethod:
            if (c == ' ') {
                parse_method(frag);
                frag->state = HttpFragStatePath;
                frag->buf.max = ECS_HTTP_REQUEST_LEN_MAX;
            } else {
                ecs_strbuf_appendch(&frag->buf, c);
            }
            break;
        case HttpFragStatePath:
            if (c == ' ') {
                frag->state = HttpFragStateVersion;
                ecs_strbuf_appendch(&frag->buf, '\0');
            } else {
                if (c == '?' || c == '=' || c == '&') {
                    ecs_strbuf_appendch(&frag->buf, '\0');
                    int32_t offset = ecs_strbuf_written(&frag->buf);
                    if (c == '?' || c == '&') {
                        frag->param_offsets[frag->param_count] = offset;
                    } else {
                        frag->param_value_offsets[frag->param_count] = offset;
                        frag->param_count ++;
                    }
                } else {
                    ecs_strbuf_appendch(&frag->buf, c);
                }
            }
            break;
        case HttpFragStateVersion:
            if (c == '\r') {
                frag->state = HttpFragStateCR;
            } /* version is not stored */
            break;
        case HttpFragStateHeaderStart:
            if (header_writable(frag)) {
                frag->header_offsets[frag->header_count] = 
                    ecs_strbuf_written(&frag->buf);
            }
            header_buf_reset(frag);
            frag->state = HttpFragStateHeaderName;
            /* fallthrough */
        case HttpFragStateHeaderName:
            if (c == ':') {
                frag->state = HttpFragStateHeaderValueStart;
                header_buf_append(frag, '\0');
                frag->parse_content_length = !ecs_os_strcmp(
                    frag->header_buf, "Content-Length");

                if (header_writable(frag)) {
                    ecs_strbuf_appendch(&frag->buf, '\0');
                    frag->header_value_offsets[frag->header_count] =
                        ecs_strbuf_written(&frag->buf);
                }
            } else if (c == '\r') {
                frag->state = HttpFragStateCR;
            } else  {
                header_buf_append(frag, c);
                if (header_writable(frag)) {
                    ecs_strbuf_appendch(&frag->buf, c);
                }
            }
            break;
        case HttpFragStateHeaderValueStart:
            header_buf_reset(frag);
            frag->state = HttpFragStateHeaderValue;
            if (c == ' ') { /* skip first space */
                break;
            }
            /* fallthrough */
        case HttpFragStateHeaderValue:
            if (c == '\r') {
                if (frag->parse_content_length) {
                    header_buf_append(frag, '\0');
                    int32_t len = atoi(frag->header_buf);
                    if (len < 0) {
                        frag->invalid = true;
                    } else {
                        frag->content_length = len;
                    }
                    frag->parse_content_length = false;
                }
                if (header_writable(frag)) {
                    int32_t cur = ecs_strbuf_written(&frag->buf);
                    if (frag->header_offsets[frag->header_count] < cur &&
                        frag->header_value_offsets[frag->header_count] < cur)
                    {
                        ecs_strbuf_appendch(&frag->buf, '\0');
                        frag->header_count ++;
                    }
                }
                frag->state = HttpFragStateCR;
            } else {
                if (frag->parse_content_length) {
                    header_buf_append(frag, c);
                }
                if (header_writable(frag)) {
                    ecs_strbuf_appendch(&frag->buf, c);
                }
            }
            break;
        case HttpFragStateCR:
            if (c == '\n') {
                frag->state = HttpFragStateCRLF;
            } else {
                frag->state = HttpFragStateHeaderStart;
            }
            break;
        case HttpFragStateCRLF:
            if (c == '\r') {
                frag->state = HttpFragStateCRLFCR;
            } else {
                frag->state = HttpFragStateHeaderStart;
                i--;
            }
            break;
        case HttpFragStateCRLFCR:
            if (c == '\n') {
                if (frag->content_length != 0) {
                    frag->body_offset = ecs_strbuf_written(&frag->buf);
                    frag->state = HttpFragStateBody;
                } else {
                    frag->state = HttpFragStateDone;
                }
            } else {
                frag->state = HttpFragStateHeaderStart;
            }
            break;
        case HttpFragStateBody: {
                ecs_strbuf_appendch(&frag->buf, c);
                if ((ecs_strbuf_written(&frag->buf) - frag->body_offset) == 
                    frag->content_length) 
                {
                    frag->state = HttpFragStateDone;
                }
            }
            break;
        case HttpFragStateDone:
            break;
        }
    }

    if (frag->state == HttpFragStateDone) {
        frag->state = HttpFragStateBegin;
        if (conn->pub.id == conn_id) {
            enqueue_request(conn);
        }
        return true;
    } else {
        return false;
    }
}

static
void append_send_headers(
    ecs_strbuf_t *hdrs,
    int code, 
    const char* status, 
    const char* content_type,  
    ecs_strbuf_t *extra_headers,
    ecs_size_t content_len) 
{
    ecs_strbuf_appendstr(hdrs, "HTTP/1.1 ");
    ecs_strbuf_append(hdrs, "%d ", code);
    ecs_strbuf_appendstr(hdrs, status);
    ecs_strbuf_appendstr(hdrs, "\r\n");

    ecs_strbuf_appendstr(hdrs, "Content-Type: ");
    ecs_strbuf_appendstr(hdrs, content_type);
    ecs_strbuf_appendstr(hdrs, "\r\n");

    ecs_strbuf_appendstr(hdrs, "Content-Length: ");
    ecs_strbuf_append(hdrs, "%d", content_len);
    ecs_strbuf_appendstr(hdrs, "\r\n");

    ecs_strbuf_appendstr(hdrs, "Server: flecs\r\n");

    ecs_strbuf_mergebuff(hdrs, extra_headers);

    ecs_strbuf_appendstr(hdrs, "\r\n");
}

static
void send_reply(
    ecs_http_connection_impl_t* conn, 
    ecs_http_reply_t* reply) 
{
    char hdrs[ECS_HTTP_REPLY_HEADER_SIZE];
    ecs_strbuf_t hdr_buf = ECS_STRBUF_INIT;
    hdr_buf.buf = hdrs;
    hdr_buf.max = ECS_HTTP_REPLY_HEADER_SIZE;
    hdr_buf.buf = hdrs;

    char *content = ecs_strbuf_get(&reply->body);
    int32_t content_length = reply->body.length - 1;

    /* First, send the response HTTP headers */
    append_send_headers(&hdr_buf, reply->code, reply->status, 
        reply->content_type, &reply->headers, content_length);

    ecs_size_t hdrs_len = ecs_strbuf_written(&hdr_buf);
    hdrs[hdrs_len] = '\0';
    ecs_size_t written = http_send(conn->sock, hdrs, hdrs_len, 0);

    if (written != hdrs_len) {
        ecs_err("failed to write HTTP response headers to '%s:%s': %s",
            conn->pub.host, conn->pub.port, ecs_os_strerror(errno));
        return;
    }

    /* Second, send response body */
    if (content_length > 0) {
        written = http_send(conn->sock, content, content_length, 0);
        if (written != content_length) {
            ecs_err("failed to write HTTP response body to '%s:%s': %s",
                conn->pub.host, conn->pub.port, ecs_os_strerror(errno));
        }
    }
}

static
void recv_request(
    ecs_http_server_t *srv,
    ecs_http_connection_impl_t *conn, 
    uint64_t conn_id,
    ecs_http_socket_t sock)
{
    ecs_size_t bytes_read;
    char recv_buf[ECS_HTTP_SEND_RECV_BUFFER_SIZE];

    while ((bytes_read = http_recv(
        sock, recv_buf, ECS_SIZEOF(recv_buf), 0)) > 0) 
    {
        ecs_os_mutex_lock(srv->lock);
        bool is_alive = conn->pub.id == conn_id;
        if (is_alive) {
            conn->dequeue_timeout = 0;
            conn->dequeue_retries = 0;
        }
        ecs_os_mutex_unlock(srv->lock);

        if (is_alive) {
            if (parse_request(conn, conn_id, recv_buf, bytes_read)) {
                return;
            }
        } else {
            return;
        }
    }
}

static
void init_connection(
    ecs_http_server_t *srv, 
    ecs_http_socket_t sock_conn,
    struct sockaddr_storage *remote_addr, 
    ecs_size_t remote_addr_len) 
{
    /* Create new connection */
    ecs_os_mutex_lock(srv->lock);
    ecs_http_connection_impl_t *conn = flecs_sparse_add(
        srv->connections, ecs_http_connection_impl_t);
    uint64_t conn_id = conn->pub.id = flecs_sparse_last_id(srv->connections);
    conn->pub.server = srv;
    conn->sock = sock_conn;
    ecs_os_mutex_unlock(srv->lock);

    char *remote_host = conn->pub.host;
    char *remote_port = conn->pub.port;

    /* Fetch name & port info */
    if (http_getnameinfo((struct sockaddr*) remote_addr, remote_addr_len,
        remote_host, ECS_SIZEOF(conn->pub.host),
        remote_port, ECS_SIZEOF(conn->pub.port),
            NI_NUMERICHOST | NI_NUMERICSERV))
    {
        ecs_os_strcpy(remote_host, "unknown");
        ecs_os_strcpy(remote_port, "unknown");
    }

    ecs_dbg_2("http: connection established from '%s:%s'", 
        remote_host, remote_port);

    recv_request(srv, conn, conn_id, sock_conn);

    ecs_dbg_2("http: request received from '%s:%s'", 
        remote_host, remote_port);
}

static
void accept_connections(
    ecs_http_server_t* srv, 
    const struct sockaddr* addr, 
    ecs_size_t addr_len) 
{
#ifdef ECS_TARGET_WINDOWS
    /* If on Windows, test if winsock needs to be initialized */
    SOCKET testsocket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
    if (SOCKET_ERROR == testsocket && WSANOTINITIALISED == WSAGetLastError()) {
        WSADATA data = { 0 };
        int result = WSAStartup(MAKEWORD(2, 2), &data);
        if (result) {
            ecs_warn("WSAStartup failed with GetLastError = %d\n", 
                GetLastError());
            return;
        }
    } else {
        http_close(&testsocket);
    }
#endif

    /* Resolve name + port (used for logging) */
    char addr_host[256];
    char addr_port[20];

    ecs_http_socket_t sock = HTTP_SOCKET_INVALID;
    ecs_assert(srv->sock == HTTP_SOCKET_INVALID, ECS_INTERNAL_ERROR, NULL);

    if (http_getnameinfo(
        addr, addr_len, addr_host, ECS_SIZEOF(addr_host), addr_port, 
        ECS_SIZEOF(addr_port), NI_NUMERICHOST | NI_NUMERICSERV))
    {
        ecs_os_strcpy(addr_host, "unknown");
        ecs_os_strcpy(addr_port, "unknown");
    }

    ecs_os_mutex_lock(srv->lock);
    if (srv->should_run) {
        ecs_dbg_2("http: initializing connection socket");

        sock = socket(addr->sa_family, SOCK_STREAM, IPPROTO_TCP);
        if (!http_socket_is_valid(sock)) {
            ecs_err("unable to create new connection socket: %s", 
                ecs_os_strerror(errno));
            ecs_os_mutex_unlock(srv->lock);
            goto done;
        }

        int reuse = 1;
        int result = setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, 
            (char*)&reuse, ECS_SIZEOF(reuse)); 
        if (result) {
            ecs_warn("failed to setsockopt: %s", ecs_os_strerror(errno));
        }

        if (addr->sa_family == AF_INET6) {
            int ipv6only = 0;
            if (setsockopt(sock, IPPROTO_IPV6, IPV6_V6ONLY, 
                (char*)&ipv6only, ECS_SIZEOF(ipv6only)))
            {
                ecs_warn("failed to setsockopt: %s", ecs_os_strerror(errno));
            }
        }

        result = http_bind(sock, addr, addr_len);
        if (result) {
            ecs_err("http: failed to bind to '%s:%s': %s", 
                addr_host, addr_port, ecs_os_strerror(errno));
            ecs_os_mutex_unlock(srv->lock);
            goto done;
        }

        srv->sock = sock;

        result = listen(srv->sock, SOMAXCONN);
        if (result) {
            ecs_warn("http: could not listen for SOMAXCONN (%d) connections: %s", 
                SOMAXCONN, ecs_os_strerror(errno));
        }

        ecs_trace("http: listening for incoming connections on '%s:%s'",
            addr_host, addr_port);
    } else {
        ecs_dbg_2("http: server shut down while initializing");
    }
    ecs_os_mutex_unlock(srv->lock);

    ecs_http_socket_t sock_conn;
    struct sockaddr_storage remote_addr;
    ecs_size_t remote_addr_len;

    while (srv->should_run) {
        remote_addr_len = ECS_SIZEOF(remote_addr);
        sock_conn = http_accept(srv->sock, (struct sockaddr*) &remote_addr, 
            &remote_addr_len);

        if (sock_conn == -1) {
            if (srv->should_run) {
                ecs_dbg("http: connection attempt failed: %s", 
                    ecs_os_strerror(errno));
            }
            continue;
        }

        init_connection(srv, sock_conn, &remote_addr, remote_addr_len);
    }

done:
    ecs_os_mutex_lock(srv->lock);
    if (http_socket_is_valid(srv->sock) && errno != EBADF) {
        http_close(&sock);
        srv->sock = sock;
    }
    ecs_os_mutex_unlock(srv->lock);

    ecs_trace("http: no longer accepting connections on '%s:%s'",
        addr_host, addr_port);
}

static
void* http_server_thread(void* arg) {
    ecs_http_server_t *srv = arg;
    struct sockaddr_in addr;
    ecs_os_zeromem(&addr);
    addr.sin_family = AF_INET;
    addr.sin_port = htons(srv->port);

    if (!srv->ipaddr) {
        addr.sin_addr.s_addr = htonl(INADDR_ANY);
    } else {
        inet_pton(AF_INET, srv->ipaddr, &(addr.sin_addr));
    }

    accept_connections(srv, (struct sockaddr*)&addr, ECS_SIZEOF(addr));
    return NULL;
}

static
void handle_request(
    ecs_http_server_t *srv,
    ecs_http_request_impl_t *req)
{
    ecs_http_reply_t reply = ECS_HTTP_REPLY_INIT;
    ecs_http_connection_impl_t *conn = 
        (ecs_http_connection_impl_t*)req->pub.conn;

    if (srv->callback((ecs_http_request_t*)req, &reply, srv->ctx) == false) {
        reply.code = 404;
        reply.status = "Resource not found";
    }

    send_reply(conn, &reply);
    ecs_dbg_2("http: reply sent to '%s:%s'", conn->pub.host, conn->pub.port);

    reply_free(&reply);
    request_free(req);
    connection_free(conn);
}

static
int32_t dequeue_requests(
    ecs_http_server_t *srv,
    float delta_time)
{
    ecs_os_mutex_lock(srv->lock);

    int32_t i, request_count = flecs_sparse_count(srv->requests);
    for (i = request_count - 1; i >= 1; i --) {
        ecs_http_request_impl_t *req = flecs_sparse_get_dense(
            srv->requests, ecs_http_request_impl_t, i);
        handle_request(srv, req);
    }

    int32_t connections_count = flecs_sparse_count(srv->connections);
    for (i = connections_count - 1; i >= 1; i --) {
        ecs_http_connection_impl_t *conn = flecs_sparse_get_dense(
            srv->connections, ecs_http_connection_impl_t, i);

        conn->dequeue_timeout += delta_time;
        conn->dequeue_retries ++;
        
        if ((conn->dequeue_timeout > 
            (ecs_ftime_t)ECS_HTTP_CONNECTION_PURGE_TIMEOUT) &&
             (conn->dequeue_retries > ECS_HTTP_CONNECTION_PURGE_RETRY_COUNT)) 
        {
            ecs_dbg("http: purging connection '%s:%s' (sock = %d)", 
                conn->pub.host, conn->pub.port, conn->sock);
            connection_free(conn);
        }
    }

    ecs_os_mutex_unlock(srv->lock);

    return request_count - 1;
}

const char* ecs_http_get_header(
    const ecs_http_request_t* req,
    const char* name) 
{
    for (ecs_size_t i = 0; i < req->header_count; i++) {
        if (!ecs_os_strcmp(req->headers[i].key, name)) {
            return req->headers[i].value;
        }
    }
    return NULL;
}

const char* ecs_http_get_param(
    const ecs_http_request_t* req,
    const char* name) 
{
    for (ecs_size_t i = 0; i < req->param_count; i++) {
        if (!ecs_os_strcmp(req->params[i].key, name)) {
            return req->params[i].value;
        }
    }
    return NULL;
}

ecs_http_server_t* ecs_http_server_init(
    const ecs_http_server_desc_t *desc) 
{
    ecs_check(ecs_os_has_threading(), ECS_UNSUPPORTED, 
        "missing OS API implementation");

    ecs_http_server_t* srv = ecs_os_calloc_t(ecs_http_server_t);
    srv->lock = ecs_os_mutex_new();
    srv->sock = HTTP_SOCKET_INVALID;

    srv->should_run = false;
    srv->initialized = true;

    srv->callback = desc->callback;
    srv->ctx = desc->ctx;
    srv->port = desc->port;
    srv->ipaddr = desc->ipaddr;

    srv->connections = flecs_sparse_new(ecs_http_connection_impl_t);
    srv->requests = flecs_sparse_new(ecs_http_request_impl_t);

    /* Start at id 1 */
    flecs_sparse_new_id(srv->connections);
    flecs_sparse_new_id(srv->requests);

#ifndef ECS_TARGET_WINDOWS
    /* Ignore pipe signal. SIGPIPE can occur when a message is sent to a client
     * but te client already disconnected. */
    signal(SIGPIPE, SIG_IGN);
#endif

    return srv;
error:
    return NULL;
}

void ecs_http_server_fini(
    ecs_http_server_t* srv) 
{
    if (srv->should_run) {
        ecs_http_server_stop(srv);
    }
    ecs_os_mutex_free(srv->lock);
    flecs_sparse_free(srv->connections);
    flecs_sparse_free(srv->requests);
    ecs_os_free(srv);
}

int ecs_http_server_start(
    ecs_http_server_t *srv)
{
    ecs_check(srv != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(srv->initialized, ECS_INVALID_PARAMETER, NULL);
    ecs_check(!srv->should_run, ECS_INVALID_PARAMETER, NULL);
    ecs_check(!srv->thread, ECS_INVALID_PARAMETER, NULL);

    srv->should_run = true;

    ecs_dbg("http: starting server thread");

    srv->thread = ecs_os_thread_new(http_server_thread, srv);
    if (!srv->thread) {
        goto error;
    }

    return 0;
error:
    return -1;
}

void ecs_http_server_stop(
    ecs_http_server_t* srv) 
{
    ecs_check(srv != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(srv->initialized, ECS_INVALID_OPERATION, NULL);
    ecs_check(srv->should_run, ECS_INVALID_PARAMETER, NULL);

    /* Stop server thread */
    ecs_dbg("http: shutting down server thread");

    ecs_os_mutex_lock(srv->lock);
    srv->should_run = false;
    if (http_socket_is_valid(srv->sock)) {
        http_close(&srv->sock);
    }
    ecs_os_mutex_unlock(srv->lock);

    ecs_os_thread_join(srv->thread);
    ecs_trace("http: server thread shut down");

    /* Cleanup all outstanding requests */
    int i, count = flecs_sparse_count(srv->requests);
    for (i = count - 1; i >= 1; i --) {
        request_free(flecs_sparse_get_dense(
            srv->requests, ecs_http_request_impl_t, i));
    }

    /* Close all connections */
    count = flecs_sparse_count(srv->connections);
    for (i = count - 1; i >= 1; i --) {
        connection_free(flecs_sparse_get_dense(
            srv->connections, ecs_http_connection_impl_t, i));
    }

    ecs_assert(flecs_sparse_count(srv->connections) == 1, 
        ECS_INTERNAL_ERROR, NULL);
    ecs_assert(flecs_sparse_count(srv->requests) == 1,
        ECS_INTERNAL_ERROR, NULL);

    srv->thread = 0;
error:
    return;
}

void ecs_http_server_dequeue(
    ecs_http_server_t* srv,
    float delta_time)
{
    ecs_check(srv != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(srv->initialized, ECS_INVALID_PARAMETER, NULL);
    ecs_check(srv->should_run, ECS_INVALID_PARAMETER, NULL);
    
    srv->dequeue_timeout += delta_time;
    srv->stats_timeout += delta_time;

    if ((1000 * srv->dequeue_timeout) > 
        (ecs_ftime_t)ECS_HTTP_MIN_DEQUEUE_INTERVAL) 
    {
        srv->dequeue_timeout = 0;

        ecs_time_t t = {0};
        ecs_time_measure(&t);
        int32_t request_count = dequeue_requests(srv, srv->dequeue_timeout);
        srv->requests_processed += request_count;
        srv->requests_processed_total += request_count;
        ecs_ftime_t time_spent = (ecs_ftime_t)ecs_time_measure(&t);
        srv->request_time += time_spent;
        srv->request_time_total += time_spent;
        srv->dequeue_count ++;
    }

    if ((1000 * srv->stats_timeout) > 
        (ecs_ftime_t)ECS_HTTP_MIN_STATS_INTERVAL) 
    {
        srv->stats_timeout = 0;
        ecs_dbg("http: processed %d requests in %.3fs (avg %.3fs / dequeue)",
            srv->requests_processed, (double)srv->request_time, 
            (double)(srv->request_time / (ecs_ftime_t)srv->dequeue_count));
        srv->requests_processed = 0;
        srv->request_time = 0;
        srv->dequeue_count = 0;
    }

error:
    return;
}

#endif



#ifdef FLECS_DOC

static ECS_COPY(EcsDocDescription, dst, src, {
    ecs_os_strset((char**)&dst->value, src->value);

})

static ECS_MOVE(EcsDocDescription, dst, src, {
    ecs_os_free((char*)dst->value);
    dst->value = src->value;
    src->value = NULL;
})

static ECS_DTOR(EcsDocDescription, ptr, { 
    ecs_os_free((char*)ptr->value);
})

void ecs_doc_set_name(
    ecs_world_t *world,
    ecs_entity_t entity,
    const char *name)
{
    ecs_set_pair(world, entity, EcsDocDescription, EcsName, {
        .value = (char*)name
    });
}

void ecs_doc_set_brief(
    ecs_world_t *world,
    ecs_entity_t entity,
    const char *description)
{
    ecs_set_pair(world, entity, EcsDocDescription, EcsDocBrief, {
        .value = (char*)description
    });
}

void ecs_doc_set_detail(
    ecs_world_t *world,
    ecs_entity_t entity,
    const char *description)
{
    ecs_set_pair(world, entity, EcsDocDescription, EcsDocDetail, {
        .value = (char*)description
    });
}

void ecs_doc_set_link(
    ecs_world_t *world,
    ecs_entity_t entity,
    const char *link)
{
    ecs_set_pair(world, entity, EcsDocDescription, EcsDocLink, {
        .value = (char*)link
    });
}

void ecs_doc_set_color(
    ecs_world_t *world,
    ecs_entity_t entity,
    const char *color)
{
    ecs_set_pair(world, entity, EcsDocDescription, EcsDocColor, {
        .value = (char*)color
    });
}

const char* ecs_doc_get_name(
    const ecs_world_t *world,
    ecs_entity_t entity)
{
    const EcsDocDescription *ptr = ecs_get_pair(
        world, entity, EcsDocDescription, EcsName);
    if (ptr) {
        return ptr->value;
    } else {
        return ecs_get_name(world, entity);
    }
}

const char* ecs_doc_get_brief(
    const ecs_world_t *world,
    ecs_entity_t entity)
{
    const EcsDocDescription *ptr = ecs_get_pair(
        world, entity, EcsDocDescription, EcsDocBrief);
    if (ptr) {
        return ptr->value;
    } else {
        return NULL;
    }
}

const char* ecs_doc_get_detail(
    const ecs_world_t *world,
    ecs_entity_t entity)
{
    const EcsDocDescription *ptr = ecs_get_pair(
        world, entity, EcsDocDescription, EcsDocDetail);
    if (ptr) {
        return ptr->value;
    } else {
        return NULL;
    }
}

const char* ecs_doc_get_link(
    const ecs_world_t *world,
    ecs_entity_t entity)
{
    const EcsDocDescription *ptr = ecs_get_pair(
        world, entity, EcsDocDescription, EcsDocLink);
    if (ptr) {
        return ptr->value;
    } else {
        return NULL;
    }
}

const char* ecs_doc_get_color(
    const ecs_world_t *world,
    ecs_entity_t entity)
{
    const EcsDocDescription *ptr = ecs_get_pair(
        world, entity, EcsDocDescription, EcsDocColor);
    if (ptr) {
        return ptr->value;
    } else {
        return NULL;
    }
}

void FlecsDocImport(
    ecs_world_t *world)
{    
    ECS_MODULE(world, FlecsDoc);

    ecs_set_name_prefix(world, "EcsDoc");

    flecs_bootstrap_component(world, EcsDocDescription);
    flecs_bootstrap_tag(world, EcsDocBrief);
    flecs_bootstrap_tag(world, EcsDocDetail);
    flecs_bootstrap_tag(world, EcsDocLink);
    flecs_bootstrap_tag(world, EcsDocColor);

    ecs_set_hooks(world, EcsDocDescription, { 
        .ctor = ecs_default_ctor,
        .move = ecs_move(EcsDocDescription),
        .copy = ecs_copy(EcsDocDescription),
        .dtor = ecs_dtor(EcsDocDescription)
    });

    ecs_add_id(world, ecs_id(EcsDocDescription), EcsDontInherit);
}

#endif


#ifdef FLECS_PARSER

#include <ctype.h>

#define ECS_ANNOTATION_LENGTH_MAX (16)

#define TOK_NEWLINE '\n'
#define TOK_COLON ':'
#define TOK_AND ','
#define TOK_OR "||"
#define TOK_NOT '!'
#define TOK_OPTIONAL '?'
#define TOK_BITWISE_OR '|'
#define TOK_BRACKET_OPEN '['
#define TOK_BRACKET_CLOSE ']'
#define TOK_WILDCARD '*'
#define TOK_VARIABLE '$'
#define TOK_PAREN_OPEN '('
#define TOK_PAREN_CLOSE ')'

#define TOK_SELF "self"
#define TOK_UP "up"
#define TOK_DOWN "down"
#define TOK_CASCADE "cascade"
#define TOK_PARENT "parent"

#define TOK_OVERRIDE "OVERRIDE"

#define TOK_ROLE_AND "AND"
#define TOK_ROLE_OR "OR"
#define TOK_ROLE_XOR "XOR"
#define TOK_ROLE_NOT "NOT"
#define TOK_ROLE_DISABLED "DISABLED"

#define TOK_IN "in"
#define TOK_OUT "out"
#define TOK_INOUT "inout"
#define TOK_INOUT_NONE "none"

#define ECS_MAX_TOKEN_SIZE (256)

typedef char ecs_token_t[ECS_MAX_TOKEN_SIZE];

const char* ecs_parse_eol_and_whitespace(
    const char *ptr)
{
    while (isspace(*ptr)) {
        ptr ++;
    }

    return ptr;    
}

/** Skip spaces when parsing signature */
const char* ecs_parse_whitespace(
    const char *ptr)
{
    while ((*ptr != '\n') && isspace(*ptr)) {
        ptr ++;
    }

    return ptr;
}

const char* ecs_parse_digit(
    const char *ptr,
    char *token)
{
    char *tptr = token;
    char ch = ptr[0];

    if (!isdigit(ch) && ch != '-') {
        ecs_parser_error(NULL, NULL, 0, "invalid start of number '%s'", ptr);
        return NULL;
    }

    tptr[0] = ch;
    tptr ++;
    ptr ++;

    for (; (ch = *ptr); ptr ++) {
        if (!isdigit(ch)) {
            break;
        }

        tptr[0] = ch;
        tptr ++;
    }

    tptr[0] = '\0';
    
    return ptr;
}

static
bool is_newline_comment(
    const char *ptr)
{
    if (ptr[0] == '/' && ptr[1] == '/') {
        return true;
    }
    return false;
}

const char* ecs_parse_fluff(
    const char *ptr,
    char **last_comment)
{
    const char *last_comment_start = NULL;

    do {
        /* Skip whitespaces before checking for a comment */
        ptr = ecs_parse_whitespace(ptr);

        /* Newline comment, skip until newline character */
        if (is_newline_comment(ptr)) {
            ptr += 2;
            last_comment_start = ptr;

            while (ptr[0] && ptr[0] != TOK_NEWLINE) {
                ptr ++;
            }
        }

        /* If a newline character is found, skip it */
        if (ptr[0] == TOK_NEWLINE) {
            ptr ++;
        }

    } while (isspace(ptr[0]) || is_newline_comment(ptr));

    if (last_comment) {
        *last_comment = (char*)last_comment_start;
    }

    return ptr;
}

/* -- Private functions -- */

static
bool valid_identifier_start_char(
    char ch)
{
    if (ch && (isalpha(ch) || (ch == '_') || (ch == '*') ||
        (ch == '0') || (ch == TOK_VARIABLE) || isdigit(ch))) 
    {
        return true;
    }

    return false;
}

static
bool valid_token_start_char(
    char ch)
{
    if ((ch == '"') || (ch == '{') || (ch == '}') || (ch == ',') || (ch == '-')
        || (ch == '[') || (ch == ']') || valid_identifier_start_char(ch))
    {
        return true;
    }

    return false;
}

static
bool valid_token_char(
    char ch)
{
    if (ch && 
        (isalpha(ch) || isdigit(ch) || ch == '_' || ch == '.' || ch == '"')) 
    {
        return true;
    }

    return false;
}

static
bool valid_operator_char(
    char ch)
{
    if (ch == TOK_OPTIONAL || ch == TOK_NOT) {
        return true;
    }

    return false;
}

const char* ecs_parse_token(
    const char *name,
    const char *expr,
    const char *ptr,
    char *token_out)
{
    int64_t column = ptr - expr;

    ptr = ecs_parse_whitespace(ptr);
    char *tptr = token_out, ch = ptr[0];

    if (!valid_token_start_char(ch)) {
        if (ch == '\0' || ch == '\n') {
            ecs_parser_error(name, expr, column, 
                "unexpected end of expression");
        } else {
            ecs_parser_error(name, expr, column, 
                "invalid start of token '%s'", ptr);
        }
        return NULL;
    }

    tptr[0] = ch;
    tptr ++;
    ptr ++;

    if (ch == '{' || ch == '}' || ch == '[' || ch == ']' || ch == ',') {
        tptr[0] = 0;
        return ptr;
    }

    int tmpl_nesting = 0;
    bool in_str = ch == '"';

    for (; (ch = *ptr); ptr ++) {
        if (ch == '<') {
            tmpl_nesting ++;
        } else if (ch == '>') {
            if (!tmpl_nesting) {
                break;
            }
            tmpl_nesting --;
        } else if (ch == '"') {
            in_str = !in_str;
        } else
        if (!valid_token_char(ch) && !in_str) {
            break;
        }

        tptr[0] = ch;
        tptr ++;
    }

    tptr[0] = '\0';

    if (tmpl_nesting != 0) {
        ecs_parser_error(name, expr, column, 
            "identifier '%s' has mismatching < > pairs", ptr);
        return NULL;
    }

    const char *next_ptr = ecs_parse_whitespace(ptr);
    if (next_ptr[0] == ':' && next_ptr != ptr) {
        /* Whitespace between token and : is significant */
        ptr = next_ptr - 1;
    } else {
        ptr = next_ptr;
    }

    return ptr;
}

static
const char* ecs_parse_identifier(
    const char *name,
    const char *expr,
    const char *ptr,
    char *token_out)
{
    if (!valid_identifier_start_char(ptr[0])) {
        ecs_parser_error(name, expr, (ptr - expr), 
            "expected start of identifier");
        return NULL;
    }

    ptr = ecs_parse_token(name, expr, ptr, token_out);

    return ptr;
}

static
int parse_identifier(
    const char *token,
    ecs_term_id_t *out)
{
    const char *tptr = token;
    if (tptr[0] == TOK_VARIABLE && tptr[1]) {
        out->flags |= EcsIsVariable;
        tptr ++;
    }

    out->name = ecs_os_strdup(tptr);

    return 0;
}

static
ecs_entity_t parse_role(
    const char *name,
    const char *sig,
    int64_t column,
    const char *token)
{
    if (!ecs_os_strcmp(token, TOK_ROLE_AND)) {
        return ECS_AND;
    } else if (!ecs_os_strcmp(token, TOK_ROLE_OR)) {
        return ECS_OR;
    } else if (!ecs_os_strcmp(token, TOK_ROLE_XOR)) {
        return ECS_XOR;
    } else if (!ecs_os_strcmp(token, TOK_ROLE_NOT)) {
        return ECS_NOT;
    } else if (!ecs_os_strcmp(token, TOK_OVERRIDE)) {
        return ECS_OVERRIDE;
    } else if (!ecs_os_strcmp(token, TOK_ROLE_DISABLED)) {
        return ECS_DISABLED;        
    } else {
        ecs_parser_error(name, sig, column, "invalid role '%s'", token);
        return 0;
    }
}

static
ecs_oper_kind_t parse_operator(
    char ch)
{
    if (ch == TOK_OPTIONAL) {
        return EcsOptional;
    } else if (ch == TOK_NOT) {
        return EcsNot;
    } else {
        ecs_abort(ECS_INTERNAL_ERROR, NULL);
    }
}

static
const char* parse_annotation(
    const char *name,
    const char *sig,
    int64_t column,
    const char *ptr, 
    ecs_inout_kind_t *inout_kind_out)
{
    char token[ECS_MAX_TOKEN_SIZE];

    ptr = ecs_parse_identifier(name, sig, ptr, token);
    if (!ptr) {
        return NULL;
    }

    if (!ecs_os_strcmp(token, TOK_IN)) {
        *inout_kind_out = EcsIn;
    } else
    if (!ecs_os_strcmp(token, TOK_OUT)) {
        *inout_kind_out = EcsOut;
    } else
    if (!ecs_os_strcmp(token, TOK_INOUT)) {
        *inout_kind_out = EcsInOut;
    } else if (!ecs_os_strcmp(token, TOK_INOUT_NONE)) {
        *inout_kind_out = EcsInOutNone;
    }

    ptr = ecs_parse_whitespace(ptr);

    if (ptr[0] != TOK_BRACKET_CLOSE) {
        ecs_parser_error(name, sig, column, "expected ]");
        return NULL;
    }

    return ptr + 1;
}

static
uint8_t parse_set_token(
    const char *token)
{
    if (!ecs_os_strcmp(token, TOK_SELF)) {
        return EcsSelf;
    } else if (!ecs_os_strcmp(token, TOK_UP)) {
        return EcsUp;
    } else if (!ecs_os_strcmp(token, TOK_DOWN)) {
        return EcsDown;
    } else if (!ecs_os_strcmp(token, TOK_CASCADE)) {
        return EcsCascade;
    } else if (!ecs_os_strcmp(token, TOK_PARENT)) {
        return EcsParent;
    } else {
        return 0;
    }
}

static
const char* parse_term_flags(
    const ecs_world_t *world,
    const char *name,
    const char *expr,
    int64_t column,
    const char *ptr,
    char *token,
    ecs_term_id_t *id,
    char tok_end)
{
    char token_buf[ECS_MAX_TOKEN_SIZE] = {0};
    if (!token) {
        token = token_buf;
        ptr = ecs_parse_identifier(name, expr, ptr, token);
        if (!ptr) {
            return NULL;
        }
    }

    do {
        uint8_t tok = parse_set_token(token);
        if (!tok) {
            ecs_parser_error(name, expr, column, 
                "invalid set token '%s'", token);
            return NULL;
        }

        if (id->flags & tok) {
            ecs_parser_error(name, expr, column, 
                "duplicate set token '%s'", token);
            return NULL;            
        }
        
        id->flags |= tok;

        if (ptr[0] == TOK_PAREN_OPEN) {
            ptr ++;

            /* Relationship (overrides IsA default) */
            if (!isdigit(ptr[0]) && valid_token_start_char(ptr[0])) {
                ptr = ecs_parse_identifier(name, expr, ptr, token);
                if (!ptr) {
                    return NULL;
                }         

                id->trav = ecs_lookup_fullpath(world, token);
                if (!id->trav) {
                    ecs_parser_error(name, expr, column, 
                        "unresolved identifier '%s'", token);
                    return NULL;
                }

                if (ptr[0] == TOK_AND) {
                    ptr = ecs_parse_whitespace(ptr + 1);
                } else if (ptr[0] != TOK_PAREN_CLOSE) {
                    ecs_parser_error(name, expr, column, 
                        "expected ',' or ')'");
                    return NULL;
                }
            }

            if (ptr[0] != TOK_PAREN_CLOSE) {
                ecs_parser_error(name, expr, column, "expected ')', got '%c'",
                    ptr[0]);
                return NULL;                
            } else {
                ptr = ecs_parse_whitespace(ptr + 1);
                if (ptr[0] != tok_end && ptr[0] != TOK_AND && ptr[0] != 0) {
                    ecs_parser_error(name, expr, column, 
                        "expected end of set expr");
                    return NULL;
                }
            }
        }

        /* Next token in set expression */
        if (ptr[0] == TOK_BITWISE_OR) {
            ptr ++;
            if (valid_token_start_char(ptr[0])) {
                ptr = ecs_parse_identifier(name, expr, ptr, token);
                if (!ptr) {
                    return NULL;
                }
            }

        /* End of set expression */
        } else if (ptr[0] == tok_end || ptr[0] == TOK_AND || !ptr[0]) {
            break;
        }
    } while (true);

    return ptr;
}

static
const char* parse_arguments(
    const ecs_world_t *world,
    const char *name,
    const char *expr,
    int64_t column,
    const char *ptr,
    char *token,
    ecs_term_t *term)
{
    (void)column;

    int32_t arg = 0;

    do {
        if (valid_token_start_char(ptr[0])) {
            if (arg == 2) {
                ecs_parser_error(name, expr, (ptr - expr), 
                    "too many arguments in term");
                return NULL;
            }

            ptr = ecs_parse_identifier(name, expr, ptr, token);
            if (!ptr) {
                return NULL;
            }

            ecs_term_id_t *term_id = NULL;

            if (arg == 0) {
                term_id = &term->src;
            } else if (arg == 1) {
                term_id = &term->second;
            }

            /* If token is a colon, the token is an identifier followed by a
             * set expression. */
            if (ptr[0] == TOK_COLON) {
                if (parse_identifier(token, term_id)) {
                    ecs_parser_error(name, expr, (ptr - expr), 
                        "invalid identifier '%s'", token);
                    return NULL;
                }

                ptr = ecs_parse_whitespace(ptr + 1);
                ptr = parse_term_flags(world, name, expr, (ptr - expr), ptr,
                    NULL, term_id, TOK_PAREN_CLOSE);
                if (!ptr) {
                    return NULL;
                }

            /* Check for term flags */
            } else if (!ecs_os_strcmp(token, TOK_CASCADE) ||
                !ecs_os_strcmp(token, TOK_SELF) || 
                !ecs_os_strcmp(token, TOK_UP) || 
                !ecs_os_strcmp(token, TOK_DOWN) || 
                !(ecs_os_strcmp(token, TOK_PARENT)))
            {
                ptr = parse_term_flags(world, name, expr, (ptr - expr), ptr, 
                    token, term_id, TOK_PAREN_CLOSE);
                if (!ptr) {
                    return NULL;
                }

            /* Regular identifier */
            } else if (parse_identifier(token, term_id)) {
                ecs_parser_error(name, expr, (ptr - expr), 
                    "invalid identifier '%s'", token);
                return NULL;
            }

            if (ptr[0] == TOK_AND) {
                ptr = ecs_parse_whitespace(ptr + 1);

                term->role = ECS_PAIR;

            } else if (ptr[0] == TOK_PAREN_CLOSE) {
                ptr = ecs_parse_whitespace(ptr + 1);
                break;

            } else {
                ecs_parser_error(name, expr, (ptr - expr), 
                    "expected ',' or ')'");
                return NULL;
            }

        } else {
            ecs_parser_error(name, expr, (ptr - expr), 
                "expected identifier or set expression");
            return NULL;
        }

        arg ++;

    } while (true);

    return ptr;
}

static
void parser_unexpected_char(
    const char *name,
    const char *expr,
    const char *ptr,
    char ch)
{
    if (ch && (ch != '\n')) {
        ecs_parser_error(name, expr, (ptr - expr), 
            "unexpected character '%c'", ch);
    } else {
        ecs_parser_error(name, expr, (ptr - expr), 
            "unexpected end of term");
    }
}

static
const char* parse_term(
    const ecs_world_t *world,
    const char *name,
    const char *expr,
    ecs_term_t *term_out)
{
    const char *ptr = expr;
    char token[ECS_MAX_TOKEN_SIZE] = {0};
    ecs_term_t term = { .move = true /* parser never owns resources */ };

    ptr = ecs_parse_whitespace(ptr);

    /* Inout specifiers always come first */
    if (ptr[0] == TOK_BRACKET_OPEN) {
        ptr = parse_annotation(name, expr, (ptr - expr), ptr + 1, &term.inout);
        if (!ptr) {
            goto error;
        }
        ptr = ecs_parse_whitespace(ptr);
    }

    if (valid_operator_char(ptr[0])) {
        term.oper = parse_operator(ptr[0]);
        ptr = ecs_parse_whitespace(ptr + 1);
    }

    /* If next token is the start of an identifier, it could be either a type
     * role, source or component identifier */
    if (valid_token_start_char(ptr[0])) {
        ptr = ecs_parse_identifier(name, expr, ptr, token);
        if (!ptr) {
            goto error;
        }

        /* Is token a type role? */
        if (ptr[0] == TOK_BITWISE_OR && ptr[1] != TOK_BITWISE_OR) {
            ptr ++;
            goto parse_role;
        }

        /* Is token a predicate? */
        if (ptr[0] == TOK_PAREN_OPEN) {
            goto parse_predicate;    
        }

        /* Next token must be a predicate */
        goto parse_predicate;

    /* Pair with implicit subject */
    } else if (ptr[0] == TOK_PAREN_OPEN) {
        goto parse_pair;

    /* Nothing else expected here */
    } else {
        parser_unexpected_char(name, expr, ptr, ptr[0]);
        goto error;
    }

parse_role:
    term.role = parse_role(name, expr, (ptr - expr), token);
    if (!term.role) {
        goto error;
    }

    ptr = ecs_parse_whitespace(ptr);

    /* If next token is the source token, this is an empty source */
    if (valid_token_start_char(ptr[0])) {
        ptr = ecs_parse_identifier(name, expr, ptr, token);
        if (!ptr) {
            goto error;
        }

        /* If not, it's a predicate */
        goto parse_predicate;

    } else if (ptr[0] == TOK_PAREN_OPEN) {
        goto parse_pair;
    } else {
        ecs_parser_error(name, expr, (ptr - expr), 
            "expected identifier after role");
        goto error;
    }

parse_predicate:
    if (parse_identifier(token, &term.first)) {
        ecs_parser_error(name, expr, (ptr - expr), 
            "invalid identifier '%s'", token); 
        goto error;
    }

    /* Set expression */
    if (ptr[0] == TOK_COLON) {
        ptr = ecs_parse_whitespace(ptr + 1);
        ptr = parse_term_flags(world, name, expr, (ptr - expr), ptr, NULL, 
            &term.first, TOK_COLON);
        if (!ptr) {
            goto error;
        }

        ptr = ecs_parse_whitespace(ptr);

        if (ptr[0] == TOK_AND || !ptr[0]) {
            goto parse_done;
        }

        if (ptr[0] != TOK_COLON) {
            ecs_parser_error(name, expr, (ptr - expr), 
                "unexpected token '%c' after predicate set expression", ptr[0]);
            goto error;
        }

        ptr = ecs_parse_whitespace(ptr + 1);
    } else {
        ptr = ecs_parse_whitespace(ptr);
    }

    if (ptr[0] == TOK_PAREN_OPEN) {
        ptr ++;
        if (ptr[0] == TOK_PAREN_CLOSE) {
            term.src.flags = EcsIsEntity;
            term.src.id = 0;
            ptr ++;
            ptr = ecs_parse_whitespace(ptr);
        } else {
            ptr = parse_arguments(
                world, name, expr, (ptr - expr), ptr, token, &term);
        }

        goto parse_done;
    }

    goto parse_done;

parse_pair:
    ptr = ecs_parse_identifier(name, expr, ptr + 1, token);
    if (!ptr) {
        goto error;
    }

    if (ptr[0] == TOK_AND) {
        ptr ++;
        term.src.id = EcsThis;
        term.src.flags |= EcsIsVariable;
        goto parse_pair_predicate;
    } else if (ptr[0] == TOK_PAREN_CLOSE) {
        term.src.id = EcsThis;
        term.src.flags |= EcsIsVariable;
        goto parse_pair_predicate;
    } else {
        parser_unexpected_char(name, expr, ptr, ptr[0]);
        goto error;
    }

parse_pair_predicate:
    if (parse_identifier(token, &term.first)) {
        ecs_parser_error(name, expr, (ptr - expr), 
            "invalid identifier '%s'", token); 
        goto error;
    }

    ptr = ecs_parse_whitespace(ptr);
    if (valid_token_start_char(ptr[0])) {
        ptr = ecs_parse_identifier(name, expr, ptr, token);
        if (!ptr) {
            goto error;
        }

        if (ptr[0] == TOK_PAREN_CLOSE) {
            ptr ++;
            goto parse_pair_object;
        } else {
            parser_unexpected_char(name, expr, ptr, ptr[0]);
            goto error;
        }
    } else if (ptr[0] == TOK_PAREN_CLOSE) {
        /* No object */
        ptr ++;
        goto parse_done;
    } else {
        ecs_parser_error(name, expr, (ptr - expr), 
            "expected pair object or ')'");
        goto error;
    }

parse_pair_object:
    if (parse_identifier(token, &term.second)) {
        ecs_parser_error(name, expr, (ptr - expr), 
            "invalid identifier '%s'", token); 
        goto error;
    }

    if (term.role != 0) {
        if (term.role != ECS_PAIR) {
            ecs_parser_error(name, expr, (ptr - expr), 
                "invalid combination of role '%s' with pair", 
                    ecs_role_str(term.role));
            goto error;
        }
    } else {
        term.role = ECS_PAIR;
    }

    ptr = ecs_parse_whitespace(ptr);
    goto parse_done;

parse_done:
    *term_out = term;
    return ptr;

error:
    ecs_term_fini(&term);
    *term_out = (ecs_term_t){0};
    return NULL;
}

static
bool is_valid_end_of_term(
    const char *ptr)
{
    if ((ptr[0] == TOK_AND) ||    /* another term with And operator */
        (ptr[0] == TOK_OR[0]) ||  /* another term with Or operator */
        (ptr[0] == '\n') ||       /* newlines are valid */
        (ptr[0] == '\0') ||       /* end of string */
        (ptr[0] == '/') ||        /* comment (in plecs) */
        (ptr[0] == '{') ||        /* scope (in plecs) */
        (ptr[0] == '}') ||
        (ptr[0] == ':') ||        /* inheritance (in plecs) */
        (ptr[0] == '='))          /* assignment (in plecs) */
    {
        return true;
    }
    return false;
}

char* ecs_parse_term(
    const ecs_world_t *world,
    const char *name,
    const char *expr,
    const char *ptr,
    ecs_term_t *term)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(ptr != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(term != NULL, ECS_INVALID_PARAMETER, NULL);

    ecs_term_id_t *src = &term->src;

    bool prev_or = false;
    if (ptr != expr) {
        if (ptr[0]) {
            if (ptr[0] == ',') {
                ptr ++;
            } else if (ptr[0] == '|') {
                ptr += 2;
                prev_or = true;
            } else {
                ecs_parser_error(name, expr, (ptr - expr), 
                    "invalid preceding token");
            }
        }
    }
    
    ptr = ecs_parse_eol_and_whitespace(ptr);
    if (!ptr[0]) {
        *term = (ecs_term_t){0};
        return (char*)ptr;
    }

    if (ptr == expr && !strcmp(expr, "0")) {
        return (char*)&ptr[1];
    }

    /* Parse next element */
    ptr = parse_term(world, name, ptr, term);
    if (!ptr) {
        goto error;
    }

    /* Check for $() notation */
    if (!ecs_os_strcmp(term->first.name, "$")) {
        if (term->src.name) {
            ecs_os_free(term->first.name);
            
            term->first = term->src;

            if (term->second.name) {
                term->src = term->second;       
            } else {
                term->src.id = EcsThis;
                term->src.name = NULL;
                term->src.flags |= EcsIsVariable;
            }

            term->second.name = ecs_os_strdup(term->first.name);
            term->second.flags |= EcsIsVariable;
        }
    }

    /* Post-parse consistency checks */

    /* If next token is OR, term is part of an OR expression */
    if (!ecs_os_strncmp(ptr, TOK_OR, 2) || prev_or) {
        /* An OR operator must always follow an AND or another OR */
        if (term->oper != EcsAnd) {
            ecs_parser_error(name, expr, (ptr - expr), 
                "cannot combine || with other operators");
            goto error;
        }

        term->oper = EcsOr;
    }

    /* Term must either end in end of expression, AND or OR token */
    if (!is_valid_end_of_term(ptr)) {
        ecs_parser_error(name, expr, (ptr - expr), 
            "expected end of expression or next term");
        goto error;
    }

    /* If the term just contained a 0, the expression has nothing. Ensure
     * that after the 0 nothing else follows */
    if (!ecs_os_strcmp(term->first.name, "0")) {
        if (ptr[0]) {
            ecs_parser_error(name, expr, (ptr - expr), 
                "unexpected term after 0"); 
            goto error;
        }

        if (src->flags != 0) {
            ecs_parser_error(name, expr, (ptr - expr), 
                "invalid combination of 0 with non-default subject");
            goto error;
        }

        src->flags = EcsIsEntity;
        src->id = 0;
        ecs_os_free(term->first.name);
        term->first.name = NULL;
    }

    /* Cannot combine EcsNothing with operators other than AND */
    if (term->oper != EcsAnd && ecs_term_match_0(term)) {
        ecs_parser_error(name, expr, (ptr - expr), 
            "invalid operator for empty source"); 
        goto error;
    }

    /* Verify consistency of OR expression */
    if (prev_or && term->oper == EcsOr) {
        term->oper = EcsOr;
    }

    /* Automatically assign This if entity is not assigned and the set is
     * nothing */
    if (!(src->flags & EcsIsEntity)) {
        if (!src->name) {
            if (!src->id) {
                src->id = EcsThis;
                src->flags |= EcsIsVariable;
            }
        }
    }

    if (src->name && !ecs_os_strcmp(src->name, "0")) {
        src->id = 0;
        src->flags = EcsIsEntity;
    }

    /* Process role */
    if (term->role == ECS_AND) {
        term->oper = EcsAndFrom;
        term->role = 0;
    } else if (term->role == ECS_OR) {
        term->oper = EcsOrFrom;
        term->role = 0;
    } else if (term->role == ECS_NOT) {
        term->oper = EcsNotFrom;
        term->role = 0;
    }

    ptr = ecs_parse_whitespace(ptr);

    return (char*)ptr;
error:
    if (term) {
        ecs_term_fini(term);
    }
    return NULL;
}

#endif


#ifdef FLECS_META_C

#include <ctype.h>

#define ECS_META_IDENTIFIER_LENGTH (256)

#define ecs_meta_error(ctx, ptr, ...)\
    ecs_parser_error((ctx)->name, (ctx)->desc, ptr - (ctx)->desc, __VA_ARGS__);

typedef char ecs_meta_token_t[ECS_META_IDENTIFIER_LENGTH];

typedef struct meta_parse_ctx_t {
    const char *name;
    const char *desc;
} meta_parse_ctx_t;

typedef struct meta_type_t {
    ecs_meta_token_t type;
    ecs_meta_token_t params;
    bool is_const;
    bool is_ptr;
} meta_type_t;

typedef struct meta_member_t {
    meta_type_t type;
    ecs_meta_token_t name;
    int64_t count;
    bool is_partial;
} meta_member_t;

typedef struct meta_constant_t {
    ecs_meta_token_t name;
    int64_t value;
    bool is_value_set;
} meta_constant_t;

typedef struct meta_params_t {
    meta_type_t key_type;
    meta_type_t type;
    int64_t count;
    bool is_key_value;
    bool is_fixed_size;
} meta_params_t;

static
const char* skip_scope(const char *ptr, meta_parse_ctx_t *ctx) {
    /* Keep track of which characters were used to open the scope */
    char stack[256];
    int32_t sp = 0;
    char ch;

    while ((ch = *ptr)) {
        if (ch == '(' || ch == '<') {
            stack[sp] = ch;

            sp ++;
            if (sp >= 256) {
                ecs_meta_error(ctx, ptr, "maximum level of nesting reached");
                goto error;
            }            
        } else if (ch == ')' || ch == '>') {
            sp --;
            if ((sp < 0) || (ch == '>' && stack[sp] != '<') || 
                (ch == ')' && stack[sp] != '(')) 
            {
                ecs_meta_error(ctx, ptr, "mismatching %c in identifier", ch);
                goto error;
            }
        }

        ptr ++;

        if (!sp) {
            break;
        }
    }

    return ptr;
error:
    return NULL;
}

static
const char* parse_c_digit(
    const char *ptr,
    int64_t *value_out)
{
    char token[24];
    ptr = ecs_parse_eol_and_whitespace(ptr);
    ptr = ecs_parse_digit(ptr, token);
    if (!ptr) {
        goto error;
    }

    *value_out = strtol(token, NULL, 0);

    return ecs_parse_eol_and_whitespace(ptr);
error:
    return NULL;
}

static
const char* parse_c_identifier(
    const char *ptr, 
    char *buff,
    char *params,
    meta_parse_ctx_t *ctx) 
{
    ecs_assert(ptr != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(buff != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(ctx != NULL, ECS_INTERNAL_ERROR, NULL);

    char *bptr = buff, ch;

    if (params) {
        params[0] = '\0';
    }

    /* Ignore whitespaces */
    ptr = ecs_parse_eol_and_whitespace(ptr);

    if (!isalpha(*ptr)) {
        ecs_meta_error(ctx, ptr, 
            "invalid identifier (starts with '%c')", *ptr);
        goto error;
    }

    while ((ch = *ptr) && !isspace(ch) && ch != ';' && ch != ',' && ch != ')' && ch != '>' && ch != '}') {
        /* Type definitions can contain macros or templates */
        if (ch == '(' || ch == '<') {
            if (!params) {
                ecs_meta_error(ctx, ptr, "unexpected %c", *ptr);
                goto error;
            }

            const char *end = skip_scope(ptr, ctx);
            ecs_os_strncpy(params, ptr, (ecs_size_t)(end - ptr));
            params[end - ptr] = '\0';

            ptr = end;
        } else {
            *bptr = ch;
            bptr ++;
            ptr ++;
        }
    }

    *bptr = '\0';

    if (!ch) {
        ecs_meta_error(ctx, ptr, "unexpected end of token");
        goto error;
    }

    return ptr;
error:
    return NULL;
}

static
const char * meta_open_scope(
    const char *ptr,
    meta_parse_ctx_t *ctx)    
{
    /* Skip initial whitespaces */
    ptr = ecs_parse_eol_and_whitespace(ptr);

    /* Is this the start of the type definition? */
    if (ctx->desc == ptr) {
        if (*ptr != '{') {
            ecs_meta_error(ctx, ptr, "missing '{' in struct definition");
            goto error; 
        }

        ptr ++;
        ptr = ecs_parse_eol_and_whitespace(ptr);
    }

    /* Is this the end of the type definition? */
    if (!*ptr) {
        ecs_meta_error(ctx, ptr, "missing '}' at end of struct definition");
        goto error;
    }   

    /* Is this the end of the type definition? */
    if (*ptr == '}') {
        ptr = ecs_parse_eol_and_whitespace(ptr + 1);
        if (*ptr) {
            ecs_meta_error(ctx, ptr, 
                "stray characters after struct definition");
            goto error;
        }
        return NULL;
    }

    return ptr;
error:
    return NULL;
}

static
const char* meta_parse_constant(
    const char *ptr,
    meta_constant_t *token,
    meta_parse_ctx_t *ctx)
{    
    ptr = meta_open_scope(ptr, ctx);
    if (!ptr) {
        return NULL;
    }

    token->is_value_set = false;

    /* Parse token, constant identifier */
    ptr = parse_c_identifier(ptr, token->name, NULL, ctx);
    if (!ptr) {
        return NULL;
    }

    ptr = ecs_parse_eol_and_whitespace(ptr);
    if (!ptr) {
        return NULL;
    }

    /* Explicit value assignment */
    if (*ptr == '=') {
        int64_t value = 0;
        ptr = parse_c_digit(ptr + 1, &value);
        token->value = value;
        token->is_value_set = true;
    }

    /* Expect a ',' or '}' */
    if (*ptr != ',' && *ptr != '}') {
        ecs_meta_error(ctx, ptr, "missing , after enum constant");
        goto error;
    }

    if (*ptr == ',') {
        return ptr + 1;
    } else {
        return ptr;
    }
error:
    return NULL;
}

static
const char* meta_parse_type(
    const char *ptr,
    meta_type_t *token,
    meta_parse_ctx_t *ctx)
{
    token->is_ptr = false;
    token->is_const = false;

    ptr = ecs_parse_eol_and_whitespace(ptr);

    /* Parse token, expect type identifier or ECS_PROPERTY */
    ptr = parse_c_identifier(ptr, token->type, token->params, ctx);
    if (!ptr) {
        goto error;
    }

    if (!strcmp(token->type, "ECS_PRIVATE")) {
        /* Members from this point are not stored in metadata */
        ptr += ecs_os_strlen(ptr);
        goto done;
    }

    /* If token is const, set const flag and continue parsing type */
    if (!strcmp(token->type, "const")) {
        token->is_const = true;

        /* Parse type after const */
        ptr = parse_c_identifier(ptr + 1, token->type, token->params, ctx);
    }

    /* Check if type is a pointer */
    ptr = ecs_parse_eol_and_whitespace(ptr);
    if (*ptr == '*') {
        token->is_ptr = true;
        ptr ++;
    }

done:
    return ptr;
error:
    return NULL;
}

static
const char* meta_parse_member(
    const char *ptr,
    meta_member_t *token,
    meta_parse_ctx_t *ctx)
{
    ptr = meta_open_scope(ptr, ctx);
    if (!ptr) {
        return NULL;
    }

    token->count = 1;
    token->is_partial = false;

    /* Parse member type */
    ptr = meta_parse_type(ptr, &token->type, ctx);
    if (!ptr) {
        token->is_partial = true;
        goto error;
    }

    /* Next token is the identifier */
    ptr = parse_c_identifier(ptr, token->name, NULL, ctx);
    if (!ptr) {
        goto error;
    }

    /* Skip whitespace between member and [ or ; */
    ptr = ecs_parse_eol_and_whitespace(ptr);

    /* Check if this is an array */
    char *array_start = strchr(token->name, '[');
    if (!array_start) {
        /* If the [ was separated by a space, it will not be parsed as part of
         * the name */
        if (*ptr == '[') {
            array_start = (char*)ptr; /* safe, will not be modified */
        }
    }

    if (array_start) {
        /* Check if the [ matches with a ] */
        char *array_end = strchr(array_start, ']');
        if (!array_end) {
            ecs_meta_error(ctx, ptr, "missing ']'");
            goto error;

        } else if (array_end - array_start == 0) {
            ecs_meta_error(ctx, ptr, "dynamic size arrays are not supported");
            goto error;
        }

        token->count = atoi(array_start + 1);

        if (array_start == ptr) {
            /* If [ was found after name, continue parsing after ] */
            ptr = array_end + 1;
        } else {
            /* If [ was fonud in name, replace it with 0 terminator */
            array_start[0] = '\0';
        }
    }

    /* Expect a ; */
    if (*ptr != ';') {
        ecs_meta_error(ctx, ptr, "missing ; after member declaration");
        goto error;
    }

    return ptr + 1;
error:
    return NULL;
}

static
int meta_parse_desc(
    const char *ptr,
    meta_params_t *token,
    meta_parse_ctx_t *ctx)
{
    token->is_key_value = false;
    token->is_fixed_size = false;

    ptr = ecs_parse_eol_and_whitespace(ptr);
    if (*ptr != '(' && *ptr != '<') {
        ecs_meta_error(ctx, ptr, 
            "expected '(' at start of collection definition");
        goto error;
    }

    ptr ++;

    /* Parse type identifier */
    ptr = meta_parse_type(ptr, &token->type, ctx);
    if (!ptr) {
        goto error;
    }

    ptr = ecs_parse_eol_and_whitespace(ptr);

    /* If next token is a ',' the first type was a key type */
    if (*ptr == ',') {
        ptr = ecs_parse_eol_and_whitespace(ptr + 1);
        
        if (isdigit(*ptr)) {
            int64_t value;
            ptr = parse_c_digit(ptr, &value);
            if (!ptr) {
                goto error;
            }

            token->count = value;
            token->is_fixed_size = true;
        } else {
            token->key_type = token->type;

            /* Parse element type */
            ptr = meta_parse_type(ptr, &token->type, ctx);
            ptr = ecs_parse_eol_and_whitespace(ptr);

            token->is_key_value = true;
        }
    }

    if (*ptr != ')' && *ptr != '>') {
        ecs_meta_error(ctx, ptr, 
            "expected ')' at end of collection definition");
        goto error;
    }

    return 0;
error:
    return -1;
}

static
ecs_entity_t meta_lookup(
    ecs_world_t *world,
    meta_type_t *token,
    const char *ptr,
    int64_t count,
    meta_parse_ctx_t *ctx);

static
ecs_entity_t meta_lookup_array(
    ecs_world_t *world,
    ecs_entity_t e,
    const char *params_decl,
    meta_parse_ctx_t *ctx)
{
    meta_parse_ctx_t param_ctx = {
        .name = ctx->name,
        .desc = params_decl
    };

    meta_params_t params;
    if (meta_parse_desc(params_decl, &params, &param_ctx)) {
        goto error;
    }
    if (!params.is_fixed_size) {
        ecs_meta_error(ctx, params_decl, "missing size for array");
        goto error;
    }

    if (!params.count) {
        ecs_meta_error(ctx, params_decl, "invalid array size");
        goto error;
    }

    ecs_entity_t element_type = ecs_lookup_symbol(world, params.type.type, true);
    if (!element_type) {
        ecs_meta_error(ctx, params_decl, "unknown element type '%s'",
            params.type.type);
    }

    if (!e) {
        e = ecs_new_id(world);
    }

    ecs_check(params.count <= INT32_MAX, ECS_INVALID_PARAMETER, NULL);

    return ecs_set(world, e, EcsArray, { element_type, (int32_t)params.count });
error:
    return 0;
}

static
ecs_entity_t meta_lookup_vector(
    ecs_world_t *world,
    ecs_entity_t e,
    const char *params_decl,
    meta_parse_ctx_t *ctx)
{
    meta_parse_ctx_t param_ctx = {
        .name = ctx->name,
        .desc = params_decl
    };

    meta_params_t params;
    if (meta_parse_desc(params_decl, &params, &param_ctx)) {
        goto error;
    }

    if (params.is_key_value) {
        ecs_meta_error(ctx, params_decl,
            "unexpected key value parameters for vector");
        goto error;
    }

    ecs_entity_t element_type = meta_lookup(
        world, &params.type, params_decl, 1, &param_ctx);

    if (!e) {
        e = ecs_new_id(world);
    }

    return ecs_set(world, e, EcsVector, { element_type });
error:
    return 0;
}

static
ecs_entity_t meta_lookup_bitmask(
    ecs_world_t *world,
    ecs_entity_t e,
    const char *params_decl,
    meta_parse_ctx_t *ctx)
{
    (void)e;

    meta_parse_ctx_t param_ctx = {
        .name = ctx->name,
        .desc = params_decl
    };

    meta_params_t params;
    if (meta_parse_desc(params_decl, &params, &param_ctx)) {
        goto error;
    }

    if (params.is_key_value) {
        ecs_meta_error(ctx, params_decl,
            "unexpected key value parameters for bitmask");
        goto error;
    }

    if (params.is_fixed_size) {
        ecs_meta_error(ctx, params_decl,
            "unexpected size for bitmask");
        goto error;
    }

    ecs_entity_t bitmask_type = meta_lookup(
        world, &params.type, params_decl, 1, &param_ctx);
    ecs_check(bitmask_type != 0, ECS_INVALID_PARAMETER, NULL);

#ifndef FLECS_NDEBUG
    /* Make sure this is a bitmask type */
    const EcsMetaType *type_ptr = ecs_get(world, bitmask_type, EcsMetaType);
    ecs_check(type_ptr != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(type_ptr->kind == EcsBitmaskType, ECS_INVALID_PARAMETER, NULL);
#endif

    return bitmask_type;
error:
    return 0;
}

static
ecs_entity_t meta_lookup(
    ecs_world_t *world,
    meta_type_t *token,
    const char *ptr,
    int64_t count,
    meta_parse_ctx_t *ctx)
{
    ecs_assert(world != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(token != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(ptr != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(ctx != NULL, ECS_INTERNAL_ERROR, NULL);

    const char *typename = token->type;
    ecs_entity_t type = 0;

    /* Parse vector type */
    if (!token->is_ptr) {
        if (!ecs_os_strcmp(typename, "ecs_array")) {
            type = meta_lookup_array(world, 0, token->params, ctx);

        } else if (!ecs_os_strcmp(typename, "ecs_vector") || 
                !ecs_os_strcmp(typename, "flecs::vector")) 
        {
            type = meta_lookup_vector(world, 0, token->params, ctx);

        } else if (!ecs_os_strcmp(typename, "flecs::bitmask")) {
            type = meta_lookup_bitmask(world, 0, token->params, ctx);

        } else if (!ecs_os_strcmp(typename, "flecs::byte")) {
            type = ecs_id(ecs_byte_t);

        } else if (!ecs_os_strcmp(typename, "char")) {
            type = ecs_id(ecs_char_t);

        } else if (!ecs_os_strcmp(typename, "bool") || 
                !ecs_os_strcmp(typename, "_Bool")) 
        {
            type = ecs_id(ecs_bool_t);

        } else if (!ecs_os_strcmp(typename, "int8_t")) {
            type = ecs_id(ecs_i8_t);
        } else if (!ecs_os_strcmp(typename, "int16_t")) {
            type = ecs_id(ecs_i16_t);
        } else if (!ecs_os_strcmp(typename, "int32_t")) {
            type = ecs_id(ecs_i32_t);
        } else if (!ecs_os_strcmp(typename, "int64_t")) {
            type = ecs_id(ecs_i64_t);

        } else if (!ecs_os_strcmp(typename, "uint8_t")) {
            type = ecs_id(ecs_u8_t);
        } else if (!ecs_os_strcmp(typename, "uint16_t")) {
            type = ecs_id(ecs_u16_t);
        } else if (!ecs_os_strcmp(typename, "uint32_t")) {
            type = ecs_id(ecs_u32_t);
        } else if (!ecs_os_strcmp(typename, "uint64_t")) {
            type = ecs_id(ecs_u64_t);

        } else if (!ecs_os_strcmp(typename, "float")) {
            type = ecs_id(ecs_f32_t);
        } else if (!ecs_os_strcmp(typename, "double")) {
            type = ecs_id(ecs_f64_t);

        } else if (!ecs_os_strcmp(typename, "ecs_entity_t")) {
            type = ecs_id(ecs_entity_t);

        } else if (!ecs_os_strcmp(typename, "char*")) {
            type = ecs_id(ecs_string_t);
        } else {
            type = ecs_lookup_symbol(world, typename, true);
        }
    } else {
        if (!ecs_os_strcmp(typename, "char")) {
            typename = "flecs.meta.string";
        } else
        if (token->is_ptr) {
            typename = "flecs.meta.uptr";
        } else
        if (!ecs_os_strcmp(typename, "char*") || 
            !ecs_os_strcmp(typename, "flecs::string")) 
        {
            typename = "flecs.meta.string";
        }

        type = ecs_lookup_symbol(world, typename, true);
    }

    if (count != 1) {
        ecs_check(count <= INT32_MAX, ECS_INVALID_PARAMETER, NULL);

        type = ecs_set(world, 0, EcsArray, {type, (int32_t)count});
    }

    if (!type) {
        ecs_meta_error(ctx, ptr, "unknown type '%s'", typename);
        goto error;
    }

    return type;
error:
    return 0;
}

static
int meta_parse_struct(
    ecs_world_t *world,
    ecs_entity_t t,
    const char *desc)
{
    const char *ptr = desc;
    const char *name = ecs_get_name(world, t);

    meta_member_t token;
    meta_parse_ctx_t ctx = {
        .name = name,
        .desc = ptr
    };

    ecs_entity_t old_scope = ecs_set_scope(world, t);

    while ((ptr = meta_parse_member(ptr, &token, &ctx)) && ptr[0]) {
        ecs_entity_t m = ecs_entity_init(world, &(ecs_entity_desc_t){
            .name = token.name
        });

        ecs_entity_t type = meta_lookup(
            world, &token.type, ptr, 1, &ctx);
        if (!type) {
            goto error;
        }

        ecs_set(world, m, EcsMember, {
            .type = type, 
            .count = (ecs_size_t)token.count
        });
    }

    ecs_set_scope(world, old_scope);

    return 0;
error:
    return -1;
}

static
int meta_parse_constants(
    ecs_world_t *world,
    ecs_entity_t t,
    const char *desc,
    bool is_bitmask)
{
    ecs_assert(world != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(t != 0, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(desc != NULL, ECS_INTERNAL_ERROR, NULL);

    const char *ptr = desc;
    const char *name = ecs_get_name(world, t);

    meta_parse_ctx_t ctx = {
        .name = name,
        .desc = ptr
    };

    meta_constant_t token;
    int64_t last_value = 0;

    ecs_entity_t old_scope = ecs_set_scope(world, t);

    while ((ptr = meta_parse_constant(ptr, &token, &ctx))) {
        if (token.is_value_set) {
            last_value = token.value;
        } else if (is_bitmask) {
            ecs_meta_error(&ctx, ptr,
                "bitmask requires explicit value assignment");
            goto error;
        }

        ecs_entity_t c = ecs_entity_init(world, &(ecs_entity_desc_t){
            .name = token.name
        });

        if (!is_bitmask) {
            ecs_set_pair_object(world, c, EcsConstant, ecs_i32_t, 
                {(ecs_i32_t)last_value});
        } else {
            ecs_set_pair_object(world, c, EcsConstant, ecs_u32_t, 
                {(ecs_u32_t)last_value});
        }

        last_value ++;
    }

    ecs_set_scope(world, old_scope);

    return 0;
error:
    return -1;
}

static
int meta_parse_enum(
    ecs_world_t *world,
    ecs_entity_t t,
    const char *desc)
{
    ecs_add(world, t, EcsEnum);
    return meta_parse_constants(world, t, desc, false);
}

static
int meta_parse_bitmask(
    ecs_world_t *world,
    ecs_entity_t t,
    const char *desc)
{
    ecs_add(world, t, EcsBitmask);
    return meta_parse_constants(world, t, desc, true);
}

int ecs_meta_from_desc(
    ecs_world_t *world,
    ecs_entity_t component,
    ecs_type_kind_t kind,
    const char *desc)
{
    switch(kind) {
    case EcsStructType:
        if (meta_parse_struct(world, component, desc)) {
            goto error;
        }
        break;
    case EcsEnumType:
        if (meta_parse_enum(world, component, desc)) {
            goto error;
        }
        break;
    case EcsBitmaskType:
        if (meta_parse_bitmask(world, component, desc)) {
            goto error;
        }
        break;
    default:
        break;
    }

    return 0;
error:
    return -1;
}

#endif


#ifdef FLECS_APP

static
int default_run_action(
    ecs_world_t *world,
    ecs_app_desc_t *desc)
{
    if (desc->init) {
        desc->init(world);
    }

    int result;
    while ((result = ecs_app_run_frame(world, desc)) == 0) { }

    if (result == 1) {
        return 0; /* Normal exit */
    } else {
        return result; /* Error code */
    }
}

static
int default_frame_action(
    ecs_world_t *world,
    const ecs_app_desc_t *desc)
{
    return !ecs_progress(world, desc->delta_time);
}

static ecs_app_run_action_t run_action = default_run_action;
static ecs_app_frame_action_t frame_action = default_frame_action;
static ecs_app_desc_t ecs_app_desc;

int ecs_app_run(
    ecs_world_t *world,
    ecs_app_desc_t *desc)
{
    ecs_app_desc = *desc;

    /* Don't set FPS & threads if custom run action is set, as the platform on
     * which the app is running may not support it. */
    if (run_action == default_run_action) {
        ecs_set_target_fps(world, ecs_app_desc.target_fps);
        ecs_set_threads(world, ecs_app_desc.threads);
    }

    /* REST server enables connecting to app with explorer */
    if (desc->enable_rest) {
#ifdef FLECS_REST
        ecs_set(world, EcsWorld, EcsRest, {.port = 0});
#else
        ecs_warn("cannot enable remote API, REST addon not available");
#endif
    }

    /* Monitoring periodically collects statistics */
    if (desc->enable_monitor) {
#ifdef FLECS_MONITOR
        ECS_IMPORT(world, FlecsMonitor);
#else
        ecs_warn("cannot enable monitoring, MONITOR addon not available");
#endif
    }

    return run_action(world, &ecs_app_desc);
}

int ecs_app_run_frame(
    ecs_world_t *world,
    const ecs_app_desc_t *desc)
{
    return frame_action(world, desc);
}

int ecs_app_set_run_action(
    ecs_app_run_action_t callback)
{
    if (run_action != default_run_action) {
        ecs_err("run action already set");
        return -1;
    }

    run_action = callback;

    return 0;
}

int ecs_app_set_frame_action(
    ecs_app_frame_action_t callback)
{
    if (frame_action != default_frame_action) {
        ecs_err("frame action already set");
        return -1;
    }

    frame_action = callback;

    return 0;
}

#endif


/* Roles */
const ecs_id_t ECS_PAIR =      (ECS_ROLE | (0x7Aull << 56));
const ecs_id_t ECS_OVERRIDE =  (ECS_ROLE | (0x75ull << 56));
const ecs_id_t ECS_DISABLED =  (ECS_ROLE | (0x74ull << 56));

/** 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;

const ecs_entity_t EcsQuery =                      5;
const ecs_entity_t EcsObserver =                   7;

/* System module component ids */
const ecs_entity_t EcsSystem =                     10;
const ecs_entity_t ecs_id(EcsTickSource) =         11;

/** Timer module component ids */
const ecs_entity_t ecs_id(EcsTimer) =              13;
const ecs_entity_t ecs_id(EcsRateFilter) =         14;

/** Meta module component ids */
const ecs_entity_t ecs_id(EcsMetaType) =           15;
const ecs_entity_t ecs_id(EcsMetaTypeSerialized) = 16;
const ecs_entity_t ecs_id(EcsPrimitive) =          17;
const ecs_entity_t ecs_id(EcsEnum) =               18;
const ecs_entity_t ecs_id(EcsBitmask) =            19;
const ecs_entity_t ecs_id(EcsMember) =             20;
const ecs_entity_t ecs_id(EcsStruct) =             21;
const ecs_entity_t ecs_id(EcsArray) =              22;
const ecs_entity_t ecs_id(EcsVector) =             23;
const ecs_entity_t ecs_id(EcsUnit) =               24;
const ecs_entity_t ecs_id(EcsUnitPrefix) =         25;

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

/* Relationship properties */
const ecs_entity_t EcsWildcard =              ECS_HI_COMPONENT_ID + 10;
const ecs_entity_t EcsAny =                   ECS_HI_COMPONENT_ID + 11;
const ecs_entity_t EcsThis =                  ECS_HI_COMPONENT_ID + 12;
const ecs_entity_t EcsVariable =              ECS_HI_COMPONENT_ID + 13;

const ecs_entity_t EcsTransitive =            ECS_HI_COMPONENT_ID + 14;
const ecs_entity_t EcsReflexive =             ECS_HI_COMPONENT_ID + 15;
const ecs_entity_t EcsSymmetric =             ECS_HI_COMPONENT_ID + 16;
const ecs_entity_t EcsFinal =                 ECS_HI_COMPONENT_ID + 17;
const ecs_entity_t EcsDontInherit =           ECS_HI_COMPONENT_ID + 18;
const ecs_entity_t EcsTag =                   ECS_HI_COMPONENT_ID + 19;
const ecs_entity_t EcsUnion =                 ECS_HI_COMPONENT_ID + 20;
const ecs_entity_t EcsExclusive =             ECS_HI_COMPONENT_ID + 21;
const ecs_entity_t EcsAcyclic =               ECS_HI_COMPONENT_ID + 22;
const ecs_entity_t EcsWith =                  ECS_HI_COMPONENT_ID + 23;
const ecs_entity_t EcsOneOf =                 ECS_HI_COMPONENT_ID + 24;

/* Builtin relationships */
const ecs_entity_t EcsChildOf =               ECS_HI_COMPONENT_ID + 25;
const ecs_entity_t EcsIsA =                   ECS_HI_COMPONENT_ID + 26;
const ecs_entity_t EcsDependsOn =             ECS_HI_COMPONENT_ID + 27;

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

/* Events */
const ecs_entity_t EcsOnAdd =                 ECS_HI_COMPONENT_ID + 33;
const ecs_entity_t EcsOnRemove =              ECS_HI_COMPONENT_ID + 34;
const ecs_entity_t EcsOnSet =                 ECS_HI_COMPONENT_ID + 35;
const ecs_entity_t EcsUnSet =                 ECS_HI_COMPONENT_ID + 36;
const ecs_entity_t EcsOnDelete =              ECS_HI_COMPONENT_ID + 37;
const ecs_entity_t EcsOnCreateTable =         ECS_HI_COMPONENT_ID + 38;
const ecs_entity_t EcsOnDeleteTable =         ECS_HI_COMPONENT_ID + 39;
const ecs_entity_t EcsOnTableEmpty =          ECS_HI_COMPONENT_ID + 40;
const ecs_entity_t EcsOnTableFill =           ECS_HI_COMPONENT_ID + 41;
const ecs_entity_t EcsOnCreateTrigger =       ECS_HI_COMPONENT_ID + 42;
const ecs_entity_t EcsOnDeleteTrigger =       ECS_HI_COMPONENT_ID + 43;
const ecs_entity_t EcsOnDeleteObservable =    ECS_HI_COMPONENT_ID + 44;
const ecs_entity_t EcsOnComponentHooks =      ECS_HI_COMPONENT_ID + 45;
const ecs_entity_t EcsOnDeleteTarget =        ECS_HI_COMPONENT_ID + 46;

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

/* Misc */
const ecs_entity_t EcsDefaultChildComponent = ECS_HI_COMPONENT_ID + 55;

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

const ecs_entity_t EcsPhase =                 ECS_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) =       ECS_HI_COMPONENT_ID + 80;
const ecs_entity_t ecs_id(ecs_char_t) =       ECS_HI_COMPONENT_ID + 81;
const ecs_entity_t ecs_id(ecs_byte_t) =       ECS_HI_COMPONENT_ID + 82;
const ecs_entity_t ecs_id(ecs_u8_t) =         ECS_HI_COMPONENT_ID + 83;
const ecs_entity_t ecs_id(ecs_u16_t) =        ECS_HI_COMPONENT_ID + 84;
const ecs_entity_t ecs_id(ecs_u32_t) =        ECS_HI_COMPONENT_ID + 85;
const ecs_entity_t ecs_id(ecs_u64_t) =        ECS_HI_COMPONENT_ID + 86;
const ecs_entity_t ecs_id(ecs_uptr_t) =       ECS_HI_COMPONENT_ID + 87;
const ecs_entity_t ecs_id(ecs_i8_t) =         ECS_HI_COMPONENT_ID + 88;
const ecs_entity_t ecs_id(ecs_i16_t) =        ECS_HI_COMPONENT_ID + 89;
const ecs_entity_t ecs_id(ecs_i32_t) =        ECS_HI_COMPONENT_ID + 90;
const ecs_entity_t ecs_id(ecs_i64_t) =        ECS_HI_COMPONENT_ID + 91;
const ecs_entity_t ecs_id(ecs_iptr_t) =       ECS_HI_COMPONENT_ID + 92;
const ecs_entity_t ecs_id(ecs_f32_t) =        ECS_HI_COMPONENT_ID + 93;
const ecs_entity_t ecs_id(ecs_f64_t) =        ECS_HI_COMPONENT_ID + 94;
const ecs_entity_t ecs_id(ecs_string_t) =     ECS_HI_COMPONENT_ID + 95;
const ecs_entity_t ecs_id(ecs_entity_t) =     ECS_HI_COMPONENT_ID + 96;
const ecs_entity_t EcsConstant =              ECS_HI_COMPONENT_ID + 97;
const ecs_entity_t EcsQuantity =              ECS_HI_COMPONENT_ID + 98;

/* Doc module components */
const ecs_entity_t ecs_id(EcsDocDescription) =ECS_HI_COMPONENT_ID + 100;
const ecs_entity_t EcsDocBrief =              ECS_HI_COMPONENT_ID + 101;
const ecs_entity_t EcsDocDetail =             ECS_HI_COMPONENT_ID + 102;
const ecs_entity_t EcsDocLink =               ECS_HI_COMPONENT_ID + 103;
const ecs_entity_t EcsDocColor =              ECS_HI_COMPONENT_ID + 104;

/* REST module components */
const ecs_entity_t ecs_id(EcsRest) =          ECS_HI_COMPONENT_ID + 105;

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

/* -- 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_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)) {
        ecs_assert(!(world->flags & EcsWorldReadonly) || (ecs_get_stage_count(world) <= 1), 
            ECS_INVALID_OPERATION, NULL);
        return &world->stages[0];

    } else if (ecs_poly_is(world, ecs_stage_t)) {
        ecs_stage_t *stage = (ecs_stage_t*)world;
        *world_ptr = stage->world;
        return stage;
    }
    
    return NULL;
}

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_world_t*)ecs_get_world(stage_world);
    ecs_poly_assert(world, ecs_world_t);

    bool is_readonly = ECS_BIT_IS_SET(world->flags, EcsWorldReadonly);
    bool is_deferred = ecs_is_deferred(world);

    if (!is_readonly && !is_deferred) {
        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(ecs_get_stage_count(world) <= 1, 
        ECS_INVALID_WHILE_READONLY, NULL);

    state->is_readonly = is_readonly;
    state->is_deferred = is_deferred;

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

    /* Hack around safety checks (this ought to look ugly) */
    ecs_world_t *temp_world = world;
    ecs_stage_t *stage = flecs_stage_from_world(&temp_world);
    state->defer_count = stage->defer;
    state->defer_queue = stage->defer_queue;
    state->defer_stack = stage->defer_stack;
    flecs_stack_init(&stage->defer_stack);
    state->scope = stage->scope;
    state->with = stage->with;
    stage->defer = 0;
    stage->defer_queue = NULL;
    
    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;
        if (stage->defer_queue) {
            ecs_assert(ecs_vector_count(stage->defer_queue) == 0, 
                ECS_INTERNAL_ERROR, NULL);
            ecs_vector_free(stage->defer_queue);
        }
        stage->defer_queue = state->defer_queue;
        flecs_stack_fini(&stage->defer_stack);
        stage->defer_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 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);
    ecs_monitor_t *m;
    while ((m = ecs_map_next(&it, ecs_monitor_t, NULL))) {
        if (!m->is_dirty) {
            continue;
        }

        m->is_dirty = false;

        ecs_vector_each(m->queries, ecs_query_t*, q_ptr, {
            flecs_query_notify(world, *q_ptr, &(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_initialized(monitors)) {
        ecs_monitor_t *m = ecs_map_get(monitors, ecs_monitor_t, id);
        if (m) {
            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;

    if (!ecs_map_is_initialized(monitors)) {
        ecs_map_init(monitors, ecs_monitor_t, 1);
    }

    ecs_monitor_t *m = ecs_map_ensure(monitors, ecs_monitor_t, id);
    ecs_assert(m != NULL, ECS_INTERNAL_ERROR, NULL);        

    ecs_query_t **q = ecs_vector_add(&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_initialized(monitors)) {
        return;
    }

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

    int32_t i, count = ecs_vector_count(m->queries);
    ecs_query_t **queries = ecs_vector_first(m->queries, ecs_query_t*);
    for (i = 0; i < count; i ++) {
        if (queries[i] == query) {
            ecs_vector_remove(m->queries, ecs_query_t*, i);
            count --;
            break;
        }
    }

    if (!count) {
        ecs_vector_free(m->queries);
        ecs_map_remove(monitors, id);
    }

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

static
void init_store(
    ecs_world_t *world) 
{
    ecs_os_memset(&world->store, 0, ECS_SIZEOF(ecs_store_t));
    
    /* Initialize entity index */
    flecs_sparse_init(&world->store.entity_index, ecs_record_t);
    flecs_sparse_set_id_source(&world->store.entity_index, 
        &world->info.last_id);

    /* Initialize root table */
    flecs_sparse_init(&world->store.tables, ecs_table_t);

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

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

static
void 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(&world->store.tables, 
        ecs_table_t, 0);
    ecs_assert(first->id == 0, ECS_INTERNAL_ERROR, NULL);
    (void)first;

    for (i = 1; i < count; i ++) {
        ecs_table_t *t = flecs_sparse_get_dense(&world->store.tables, 
            ecs_table_t, i);
        flecs_table_release(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(&world->store.tables, 
            ecs_table_t, i);
        flecs_table_free_type(t);
    }

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

static
void 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);

    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;

    /* 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. */
    ecs_defer_begin(world);

    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;

        /* Count backwards so that we're always deleting the last entity in the
         * table which reduces moving components around */
        for (i = count - 1; i >= 0; i --) {
            ecs_record_t *r = flecs_entities_get(world, entities[i]);
            ecs_flags32_t flags = ECS_RECORD_TO_ROW_FLAGS(r->row);
            if (!(flags & EcsEntityObservedObject)) {
                continue; /* Filter out entities that aren't objects */
            }

            ecs_delete(world, entities[i]);
        }
    }

    ecs_defer_end(world);
}

static
void fini_store(ecs_world_t *world) {
    clean_tables(world);
    flecs_sparse_fini(&world->store.tables);
    flecs_table_release(world, &world->store.root);
    flecs_sparse_clear(&world->store.entity_index);
    flecs_hashmap_fini(&world->store.table_map);
    ecs_vector_free(world->store.records);
    ecs_vector_free(world->store.marked_ids);

    ecs_graph_edge_hdr_t *cur, *next = world->store.first_free;
    while ((cur = next)) {
        next = cur->next;
        ecs_os_free(cur);
    }
}

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

    ecs_world_t *world = it->real_world;
    ecs_sparse_t *entity_index = &world->store.entity_index;
    it->entities = (ecs_entity_t*)flecs_sparse_ids(entity_index);
    it->count = flecs_sparse_count(entity_index);
    flecs_iter_validate(it);

    return true;
}

static
void 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_world_t*)world,
            .real_world = (ecs_world_t*)ecs_get_world(world),
            .next = world_iter_next
        };
    }
}

static
void 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_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_APP
        ecs_trace("FLECS_APP");
    #endif
    #ifdef FLECS_OS_API_IMPL
        ecs_trace("FLECS_OS_API_IMPL");
    #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");
    }

    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);
#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->self = world;
    world->type_info = flecs_sparse_new(ecs_type_info_t);
    ecs_map_init(&world->id_index, ecs_id_record_t*, ECS_HI_COMPONENT_ID);
    flecs_observable_init(&world->observable);
    world->iterable.init = world_iter_init;
    world->pending_tables = flecs_sparse_new(ecs_table_t*);
    world->pending_buffer = flecs_sparse_new(ecs_table_t*);
    flecs_name_index_init(&world->aliases);
    flecs_name_index_init(&world->symbols);

    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);
    init_store(world);

    flecs_bootstrap(world);

    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_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;
}

#define ARG(short, long, action)\
    if (i < argc) {\
        if (argv[i][0] == '-') {\
            if (argv[i][1] == '-') {\
                if (long && !strcmp(&argv[i][2], long ? long : "")) {\
                    action;\
                    parsed = true;\
                }\
            } else {\
                if (short && argv[i][1] == short) {\
                    action;\
                    parsed = true;\
                }\
            }\
        }\
    }

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(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 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 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 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 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 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 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 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 = default_copy_ctor;
    }

    if (h->move && !h->move_ctor) {
        ti->hooks.move_ctor = 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 = 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 = 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;
            }            
        }
    }

    if (!h->move_dtor) {
        if (h->move) {
            if (h->dtor) {
                ti->hooks.move_dtor = default_move_w_dtor;
            } else {
                ti->hooks.move_dtor = default_move;
            }
        } else {
            if (h->dtor) {
                ti->hooks.move_dtor = 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_vector_add(&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_vector_add(&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 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(tables, ecs_table_t, i);
        flecs_table_remove_actions(world, table);
    }
}

/* Invoke fini actions */
static
void fini_actions(
    ecs_world_t *world)
{
    ecs_vector_each(world->fini_actions, ecs_action_elem_t, elem, {
        elem->action(world, elem->ctx);
    });

    ecs_vector_free(world->fini_actions);
}

/* Cleanup remaining type info elements */
static
void 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(type_info, 
            ecs_type_info_t, i);
        flecs_type_info_fini(ti);
    }
    flecs_sparse_free(world->type_info);
}

ecs_entity_t flecs_get_oneof(
    const ecs_world_t *world,
    ecs_entity_t e)
{
    if (ecs_has_id(world, e, EcsOneOf)) {
        return e;
    } else {
        return ecs_get_target(world, e, EcsOneOf, 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();
    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();
    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 */
    ecs_defer_begin(world);

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

    /* This will destroy all entities and components. After this point no more
     * user code is executed. */
    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_initialized(&world->monitors.monitors), 
        ECS_INTERNAL_ERROR, NULL);

    ecs_dbg_1("#[bold]cleanup world datastructures");
    ecs_log_push_1();
    flecs_sparse_fini(&world->store.entity_index);
    flecs_fini_id_records(world);
    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();

    /* 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 + ECS_HI_COMPONENT_ID);
}

void flecs_eval_component_monitors(
    ecs_world_t *world)
{
    ecs_poly_assert(world, ecs_world_t);  
    flecs_process_pending_tables(world);  
    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 (world->info.target_fps == 0.0f || 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_context(
    const ecs_world_t *world)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);    
    world = ecs_get_world(world);
    return world->context;
error:
    return NULL;
}

void ecs_set_context(
    ecs_world_t *world,
    void *context)
{
    ecs_poly_assert(world, ecs_world_t);
    world->context = context;
}

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 > world->info.last_id, 
        ECS_INVALID_PARAMETER, NULL);

    if (world->info.last_id < id_start) {
        world->info.last_id = id_start - 1;
    }

    world->info.min_id = id_start;
    world->info.max_id = 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;
}

void ecs_set_entity_generation(
    ecs_world_t *world,
    ecs_entity_t entity_with_generation)
{
    flecs_sparse_set_generation(
        &world->store.entity_index, 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_ROLE_MASK), ECS_INTERNAL_ERROR, NULL);

    return flecs_sparse_get(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(
            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_type_info_t*)ti;
    }

    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(world->type_info, 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);
    }
}

void flecs_type_info_free(
    ecs_world_t *world,
    ecs_entity_t component)
{
    if (world->flags & EcsWorldFini) {
        /* 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_remove_get(
        world->type_info, ecs_type_info_t, component);
    if (ti) {
        flecs_type_info_fini(ti);
    }
}

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;
    ecs_ftime_t delta_time = (ecs_ftime_t)ecs_time_measure(stop);

    if (world->info.target_fps == (ecs_ftime_t)0.0) {
        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 (sleep_time != 0) {
            ecs_sleepf((double)sleep_time);
        }

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

    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) || (user_delta_time == 0)) {
        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);

                ecs_time_measure(&t);
            } else {
                ecs_time_measure(&t);
                if (world->info.target_fps != 0) {
                    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 (delta_time == 0);

        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(user_delta_time != 0 || 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 (user_delta_time == 0) {
        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_notify_queries(
    ecs_world_t *world,
    ecs_query_event_t *event)
{
    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);
    
    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);
            int32_t i, count = ecs_table_count(table);

            for (i = 0; i < count; i ++) {
                ecs_query_t *query = queries[i].poly;
                if (query->flags & EcsQueryIsSubquery) {
                    continue;
                }

                ecs_poly_assert(query, ecs_query_t);
                flecs_query_notify(world, query, event);
            }
        }
    }    
}

void flecs_delete_table(
    ecs_world_t *world,
    ecs_table_t *table)
{
    ecs_poly_assert(world, ecs_world_t); 
    flecs_table_release(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);
            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_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;
    }

    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. */
        ecs_defer_begin(world);

        for (i = 0; i < count; i ++) {
            ecs_table_t *table = flecs_sparse_get_dense(
                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)) {
                /* 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. */
                int32_t table_count = ecs_table_count(table);
                flecs_emit(world, world, &(ecs_event_desc_t){
                    .event = table_count
                        ? EcsOnTableFill 
                        : EcsOnTableEmpty
                        ,
                    .table = table,
                    .ids = &table->type,
                    .observable = world,
                    .table_event = true
                });

                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)));
}

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_set_generation(world->pending_tables, (uint32_t)table->id);
    flecs_sparse_ensure(world->pending_tables, ecs_table_t*, 
        (uint32_t)table->id)[0] = table;
}

bool ecs_id_in_use(
    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;

    ecs_time_measure(&start);
    if (time_budget_seconds != 0) {
        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->refcount > 1) {
                /* Don't delete claimed tables */
                continue;
            }

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

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

done:
    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;
}


void flecs_observable_init(
    ecs_observable_t *observable)
{
    observable->events = ecs_sparse_new(ecs_event_record_t);
}

void flecs_observable_fini(
    ecs_observable_t *observable)
{
    ecs_sparse_t *triggers = observable->events;
    int32_t i, count = flecs_sparse_count(triggers);

    for (i = 0; i < count; i ++) {
        ecs_event_record_t *et = 
            ecs_sparse_get_dense(triggers, ecs_event_record_t, i);
        ecs_assert(et != NULL, ECS_INTERNAL_ERROR, NULL);
        (void)et;

        /* All triggers should've unregistered by now */
        ecs_assert(!ecs_map_is_initialized(&et->event_ids), 
            ECS_INTERNAL_ERROR, NULL);
    }

    flecs_sparse_free(observable->events);
}

static
void notify_subset(
    ecs_world_t *world,
    ecs_iter_t *it,
    ecs_observable_t *observable,
    ecs_entity_t entity,
    ecs_entity_t event,
    const ecs_type_t *ids)
{
    ecs_id_t pair = ecs_pair(EcsWildcard, entity);
    ecs_id_record_t *idr = flecs_id_record_get(world, pair);
    if (!idr) {
        return;
    }

    /* Iterate acyclic relationships */
    ecs_id_record_t *cur = idr;
    while ((cur = cur->acyclic.next)) {
        flecs_process_pending_tables(world);

        ecs_table_cache_iter_t idt;
        if (!flecs_table_cache_iter(&cur->cache, &idt)) {
            return;
        }

        ecs_entity_t rel = ECS_PAIR_FIRST(cur->id);
        const ecs_table_record_t *tr;
        while ((tr = flecs_table_cache_next(&idt, ecs_table_record_t))) {
            ecs_table_t *table = tr->hdr.table;

            int32_t e, entity_count = ecs_table_count(table);
            it->table = table;
            it->other_table = NULL;
            it->offset = 0;
            it->count = entity_count;

            /* Treat as new event as this could invoke observers again for
            * different tables. */
            world->event_id ++;

            flecs_set_observers_notify(it, observable, ids, event,
                ecs_pair(rel, EcsWildcard));

            ecs_entity_t *entities = ecs_storage_first(&table->data.entities);
            ecs_record_t **records = ecs_storage_first(&table->data.records);

            for (e = 0; e < entity_count; e ++) {
                uint32_t flags = ECS_RECORD_TO_ROW_FLAGS(records[e]->row);
                if (flags & EcsEntityObservedAcyclic) {
                    /* Only notify for entities that are used in pairs with
                    * acyclic relationships */
                    notify_subset(world, it, observable, entities[e], event, ids);
                }
            }
        }
    }
}

void flecs_emit(
    ecs_world_t *world,
    ecs_world_t *stage,
    ecs_event_desc_t *desc)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_check(desc != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(desc->event != 0, ECS_INVALID_PARAMETER, NULL);
    ecs_check(desc->event != EcsWildcard, ECS_INVALID_PARAMETER, NULL);
    ecs_check(desc->ids != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(desc->ids->count != 0, ECS_INVALID_PARAMETER, NULL);
    ecs_check(desc->table != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(desc->observable != NULL, ECS_INVALID_PARAMETER, NULL);

    const ecs_type_t *ids = desc->ids;
    ecs_entity_t event = desc->event;
    ecs_table_t *table = desc->table;
    int32_t row = desc->offset;
    int32_t i, count = desc->count;
    ecs_entity_t relationship = desc->relationship;

    if (!count) {
        count = ecs_table_count(table) - row;
    }

    ecs_iter_t it = {
        .world = stage,
        .real_world = world,
        .table = table,
        .term_count = 1,
        .other_table = desc->other_table,
        .offset = row,
        .count = count,
        .param = (void*)desc->param,
        .flags = desc->table_event ? EcsIterTableOnly : 0
    };

    world->event_id ++;

    ecs_observable_t *observable = ecs_get_observable(desc->observable);
    ecs_check(observable != NULL, ECS_INVALID_PARAMETER, NULL);

    if (!desc->relationship) {
        flecs_observers_notify(&it, observable, ids, event);
    } else {
        flecs_set_observers_notify(&it, observable, ids, event, 
            ecs_pair(relationship, EcsWildcard));
    }

    if (count && !desc->table_event) {
        ecs_record_t **recs = ecs_storage_get_t(
            &table->data.records, ecs_record_t*, row);

        for (i = 0; i < count; i ++) {
            ecs_record_t *r = recs[i];
            if (!r) {
                /* If the event is emitted after a bulk operation, it's possible
                 * that it hasn't been populate with entities yet. */
                continue;
            }

            uint32_t flags = ECS_RECORD_TO_ROW_FLAGS(recs[i]->row);
            if (flags & EcsEntityObservedAcyclic) {
                notify_subset(world, &it, observable, ecs_storage_first_t(
                    &table->data.entities, ecs_entity_t)[row + i], event, ids);
            }
        }
    }
    
error:
    return;
}

void ecs_emit(
    ecs_world_t *stage,
    ecs_event_desc_t *desc)
{
    ecs_world_t *world = (ecs_world_t*)ecs_get_world(stage);
    flecs_emit(world, stage, desc);
}


#include <ctype.h>

ecs_filter_t ECS_FILTER_INIT = { .hdr = { .magic = ecs_filter_t_magic }};

/* Helper type for passing around context required for error messages */
typedef struct {
    const ecs_world_t *world;
    ecs_filter_t *filter;
    ecs_term_t *term;
    int32_t term_index;
} ecs_filter_finalize_ctx_t;

static
char* flecs_filter_str(
    const ecs_world_t *world,
    const ecs_filter_t *filter,
    const ecs_filter_finalize_ctx_t *ctx,
    int32_t *term_start_out);

static
void flecs_filter_error(
    const ecs_filter_finalize_ctx_t *ctx,
    const char *fmt,
    ...)
{
    va_list args;
    va_start(args, fmt);

    int32_t term_start = 0;

    char *expr = NULL;
    if (ctx->filter) {
        expr = flecs_filter_str(ctx->world, ctx->filter, ctx, &term_start);
    } else {
        expr = ecs_term_str(ctx->world, ctx->term);
    }
    char *name = NULL;
    if (ctx->filter) {
        name = ctx->filter->name;
    }
    ecs_parser_errorv(name, expr, term_start, fmt, args);
    ecs_os_free(expr);

    va_end(args);
}

static
int flecs_term_id_finalize_flags(
    ecs_term_id_t *term_id,
    ecs_filter_finalize_ctx_t *ctx)
{
    if ((term_id->flags & EcsIsEntity) && (term_id->flags & EcsIsVariable)) {
        flecs_filter_error(ctx, "cannot set both IsEntity and IsVariable");
        return -1;
    }

    if (!(term_id->flags & EcsIsEntity) && !(term_id->flags & EcsIsVariable)) {
        if (term_id->id || term_id->name) {
            if (term_id->id == EcsThis || 
                term_id->id == EcsWildcard || 
                term_id->id == EcsAny || 
                term_id->id == EcsVariable) 
            {
                /* Builtin variable ids default to variable */
                term_id->flags |= EcsIsVariable;
            } else {
                term_id->flags |= EcsIsEntity;
            }
        }
    }

    if (term_id->flags & EcsParent) {
        term_id->flags |= EcsUp;
        term_id->trav = EcsChildOf;
    }

    if ((term_id->flags & EcsCascade) && !(term_id->flags & (EcsUp|EcsDown))) {
        term_id->flags |= EcsUp;
    }

    if ((term_id->flags & (EcsUp|EcsDown)) && !term_id->trav) {
        term_id->trav = EcsIsA;
    }

    if (term_id->trav && !(term_id->flags & EcsTraverseFlags)) {
        term_id->flags |= EcsUp;
    }

    return 0;
}

static
int flecs_term_id_lookup(
    const ecs_world_t *world,
    ecs_entity_t scope,
    ecs_term_id_t *term_id,
    bool free_name,
    ecs_filter_finalize_ctx_t *ctx)
{
    char *name = term_id->name;
    if (!name) {
        return 0;
    }

    if (term_id->flags & EcsIsVariable) {
        if (!ecs_os_strcmp(name, "This")) {
            term_id->id = EcsThis;
            if (free_name) {
                ecs_os_free(term_id->name);
            }
            term_id->name = NULL;
        }
        return 0;
    }

    ecs_assert(term_id->flags & EcsIsEntity, ECS_INTERNAL_ERROR, NULL);

    if (ecs_identifier_is_0(name)) {
        if (term_id->id) {
            flecs_filter_error(ctx, "name '0' does not match entity id");
            return -1;
        }
        return 0;
    }

    ecs_entity_t e = ecs_lookup_symbol(world, name, true);
    if (scope && !e) {
        e = ecs_lookup_child(world, scope, name);
    }

    if (!e) {
        flecs_filter_error(ctx, "unresolved identifier '%s'", name);
        return -1;
    }

    if (term_id->id && term_id->id != e) {
        char *e_str = ecs_get_fullpath(world, term_id->id);
        flecs_filter_error(ctx, "name '%s' does not match term.id '%s'", 
            name, e_str);
        ecs_os_free(e_str);
        return -1;
    }

    term_id->id = e;

    if (!ecs_os_strcmp(name, "*") || !ecs_os_strcmp(name, "_") || 
        !ecs_os_strcmp(name, "$") || !ecs_os_strcmp(name, ".")) 
    {
        term_id->flags &= ~EcsIsEntity;
        term_id->flags |= EcsIsVariable;
    }

    /* Check if looked up id is alive (relevant for numerical ids) */
    if (!ecs_is_alive(world, term_id->id)) {
        flecs_filter_error(ctx, "identifier '%s' is not alive", term_id->name);
        return -1;
    }

    if (free_name) {
        ecs_os_free(name);
    }

    term_id->name = NULL;

    return 0;
}

static
int flecs_term_ids_finalize(
    const ecs_world_t *world,
    ecs_term_t *term,
    ecs_filter_finalize_ctx_t *ctx)
{
    ecs_term_id_t *src = &term->src;
    ecs_term_id_t *first = &term->first;
    ecs_term_id_t *second = &term->second;

    /* Include inherited components (like from prefabs) by default for src */
    if (!(src->flags & EcsTraverseFlags)) {
        src->flags |= EcsSelf | EcsUp;
    }

    /* Include subsets for component by default, to support inheritance */
    if (!(first->flags & EcsTraverseFlags)) {
        first->flags |= EcsSelf | EcsDown;
    }

    /* Traverse Self by default for pair target */
    if (!(second->flags & EcsTraverseFlags)) {
        second->flags |= EcsSelf;
    }

    /* Source defaults to This */
    if ((src->id == 0) && (src->name == NULL) && !(src->flags & EcsIsEntity)) {
        src->id = EcsThis;
        src->flags |= EcsIsVariable;
    }

    /* Initialize term identifier flags */
    if (flecs_term_id_finalize_flags(src, ctx)) {
        return -1;
    }
    if (flecs_term_id_finalize_flags(first, ctx)) {
        return -1;
    }

    if (flecs_term_id_finalize_flags(second, ctx)) {
        return -1;
    }

    /* Lookup term identifiers by name */
    if (flecs_term_id_lookup(world, 0, src, term->move, ctx)) {
        return -1;
    }
    if (flecs_term_id_lookup(world, 0, first, term->move, ctx)) {
        return -1;
    }

    ecs_entity_t first_id = 0;
    ecs_entity_t oneof = 0;
    if (first->flags & EcsIsEntity) {
        first_id = first->id;

        /* If first element of pair has OneOf property, lookup second element of
         * pair in the value of the OneOf property */
        oneof = flecs_get_oneof(world, first_id);
    }

    if (flecs_term_id_lookup(world, oneof, &term->second, term->move, ctx)) {
        return -1;
    }

    /* If source is 0, reset traversal flags */
    if (src->id == 0 && src->flags & EcsIsEntity) {
        src->flags &= ~EcsTraverseFlags;
        src->trav = 0;
    }
    /* If second is 0, reset traversal flags */
    if (second->id == 0 && second->flags & EcsIsEntity) {
        second->flags &= ~EcsTraverseFlags;
        second->trav = 0;
    }

    return 0;
}

static
ecs_entity_t flecs_term_id_get_entity(
    const ecs_term_id_t *term_id)
{
    if (term_id->flags & EcsIsEntity) {
        return term_id->id; /* Id is known */
    } else if (term_id->flags & EcsIsVariable) {
        /* Return wildcard for variables, as they aren't known yet */
        if (term_id->id != EcsAny) {
            /* Any variable should not use wildcard, as this would return all
             * ids matching a wildcard, whereas Any returns the first match */
            return EcsWildcard;
        } else {
            return EcsAny;
        }
    } else {
        return 0; /* Term id is uninitialized */
    }
}

static
int flecs_term_populate_id(
    ecs_term_t *term,
    ecs_filter_finalize_ctx_t *ctx)
{
    ecs_entity_t first = flecs_term_id_get_entity(&term->first);
    ecs_entity_t second = flecs_term_id_get_entity(&term->second);
    ecs_id_t role = term->role;

    if (first & ECS_ROLE_MASK) {
        return -1;
    }
    if (second & ECS_ROLE_MASK) {
        return -1;
    }

    if ((second || term->second.flags == EcsIsEntity) && !role) {
        role = term->role = ECS_PAIR;
    }

    if (!second && role != ECS_PAIR) {
        term->id = first | role;
    } else {
        if (role != ECS_PAIR) {
            flecs_filter_error(ctx, "invalid role for pair");
            return -1;
        }

        term->id = ecs_pair(first, second);
    }

    return 0;
}

static
int flecs_term_populate_from_id(
    const ecs_world_t *world,
    ecs_term_t *term,
    ecs_filter_finalize_ctx_t *ctx)
{
    ecs_entity_t first = 0;
    ecs_entity_t second = 0;
    ecs_id_t role = term->id & ECS_ROLE_MASK;

    if (!role && term->role) {
        role = term->role;
        term->id |= role;
    }

    if (term->role && term->role != role) {
        flecs_filter_error(ctx, "mismatch between term.id & term.role");
        return -1;
    }

    term->role = role;

    if (ECS_HAS_ROLE(term->id, PAIR)) {
        first = ECS_PAIR_FIRST(term->id);
        second = ECS_PAIR_SECOND(term->id);

        if (!first) {
            flecs_filter_error(ctx, "missing first element in term.id");
            return -1;
        }
        if (!second) {
            if (first != EcsChildOf) {
                flecs_filter_error(ctx, "missing second element in term.id");
                return -1;
            } else {
                /* (ChildOf, 0) is allowed so filter can be used to efficiently
                 * query for root entities */
            }
        }
    } else {
        first = term->id & ECS_COMPONENT_MASK;
        if (!first) {
            flecs_filter_error(ctx, "missing first element in term.id");
            return -1;
        }
    }

    ecs_entity_t term_first = flecs_term_id_get_entity(&term->first);
    if (term_first) {
        if (term_first != first) {
            flecs_filter_error(ctx, "mismatch between term.id and term.first");
            return -1;
        }
    } else {
        if (!(term->first.id = ecs_get_alive(world, first))) {
            term->first.id = first;
        }
    }

    ecs_entity_t term_second = flecs_term_id_get_entity(&term->second);
    if (term_second) {
        if (ecs_entity_t_lo(term_second) != second) {
            flecs_filter_error(ctx, "mismatch between term.id and term.second");
            return -1;
        }
    } else if (second) {
        if (!(term->second.id = ecs_get_alive(world, second))) {
            term->second.id = second;
        }
    }

    return 0;
}

static
int flecs_term_verify(
    const ecs_world_t *world,
    const ecs_term_t *term,
    ecs_filter_finalize_ctx_t *ctx)
{
    const ecs_term_id_t *first = &term->first;
    const ecs_term_id_t *second = &term->second;
    const ecs_term_id_t *src = &term->src;
    ecs_entity_t first_id = 0, second_id = 0;
    ecs_id_t role = term->role;
    ecs_id_t id = term->id;

    if (first->flags & EcsIsEntity) {
        first_id = first->id;
    }
    if (second->flags & EcsIsEntity) {
        second_id = second->id;
    }

    if (role != (id & ECS_ROLE_MASK)) {
        flecs_filter_error(ctx, "mismatch between term.role & term.id");
        return -1;
    }

    if (ecs_term_id_is_set(second) && (role != ECS_PAIR)) {
        flecs_filter_error(ctx, "expected PAIR flag for term with pair");
        return -1;
    } else if (!ecs_term_id_is_set(second) && (role == ECS_PAIR)) {
        if (first_id != EcsChildOf) {
            flecs_filter_error(ctx, "unexpected PAIR flag for term without pair");
            return -1;
        } else {
            /* Exception is made for ChildOf so we can use (ChildOf, 0) to match
             * all entities in the root */
        }
    }

    if (!ecs_term_id_is_set(src)) {
        flecs_filter_error(ctx, "term.src is not initialized");
        return -1;
    }

    if (!ecs_term_id_is_set(first)) {
        flecs_filter_error(ctx, "term.first is not initialized");
        return -1;
    }

    if (role == ECS_PAIR) {
        if (!ECS_PAIR_FIRST(id)) {
            flecs_filter_error(ctx, "invalid 0 for first element in pair id");
            return -1;
        }
        if ((ECS_PAIR_FIRST(id) != EcsChildOf) && !ECS_PAIR_SECOND(id)) {
            flecs_filter_error(ctx, "invalid 0 for second element in pair id");
            return -1;
        }

        if ((first->flags & EcsIsEntity) && 
            (ecs_entity_t_lo(first_id) != ECS_PAIR_FIRST(id))) 
        {
            flecs_filter_error(ctx, "mismatch between term.id and term.first");
            return -1;
        }
        if ((first->flags & EcsIsVariable) && 
            !ecs_id_is_wildcard(ECS_PAIR_FIRST(id)))
        {
            char *id_str = ecs_id_str(world, id);
            flecs_filter_error(ctx, 
                "expected wildcard for variable term.first (got %s)", id_str);
            ecs_os_free(id_str);
            return -1;
        }

        if ((second->flags & EcsIsEntity) && 
            (ecs_entity_t_lo(second_id) != ECS_PAIR_SECOND(id))) 
        {
            flecs_filter_error(ctx, "mismatch between term.id and term.second");
            return -1;
        }
        if ((second->flags & EcsIsVariable) && 
            !ecs_id_is_wildcard(ECS_PAIR_SECOND(id))) 
        {
            char *id_str = ecs_id_str(world, id);
            flecs_filter_error(ctx, 
                "expected wildcard for variable term.second (got %s)", id_str);
            ecs_os_free(id_str);
            return -1;
        }
    } else {
        ecs_entity_t component = id & ECS_COMPONENT_MASK;
        if (!component) {
            flecs_filter_error(ctx, "missing component id");
            return -1;
        }
        if ((first->flags & EcsIsEntity) && 
            (ecs_entity_t_lo(first_id) != ecs_entity_t_lo(component))) 
        {
            flecs_filter_error(ctx, "mismatch between term.id and term.first");
            return -1;
        }
        if ((first->flags & EcsIsVariable) && !ecs_id_is_wildcard(component)) {
            char *id_str = ecs_id_str(world, id);
            flecs_filter_error(ctx, 
                "expected wildcard for variable term.first (got %s)", id_str);
            ecs_os_free(id_str);
            return -1;
        }
    }

    if (first_id) {
        if (ecs_term_id_is_set(second)) {
            ecs_flags32_t mask = EcsIsEntity | EcsIsVariable;
            if ((src->flags & mask) == (second->flags & mask)) {
                bool is_same = false;
                if (src->flags & EcsIsEntity) {
                    is_same = src->id == second->id;
                } else if (src->name && second->name) {
                    is_same = !ecs_os_strcmp(src->name, second->name);
                }

                if (is_same && ecs_has_id(world, first_id, EcsAcyclic)
                    && !ecs_has_id(world, first_id, EcsReflexive)) 
                {
                    char *pred_str = ecs_get_fullpath(world, term->first.id);
                    flecs_filter_error(ctx, "term with acyclic relationship"
                        " '%s' cannot have same subject and object",
                            pred_str);
                    ecs_os_free(pred_str);
                    return -1;
                }
            }
        }

        if (second_id && !ecs_id_is_wildcard(second_id)) {
            ecs_entity_t oneof = flecs_get_oneof(world, first_id);
            if (oneof) {
                if (!ecs_has_pair(world, second_id, EcsChildOf, oneof)) {
                    char *second_str = ecs_get_fullpath(world, second_id);
                    char *oneof_str = ecs_get_fullpath(world, oneof);
                    char *id_str = ecs_id_str(world, term->id);
                    flecs_filter_error(ctx, 
                        "invalid target '%s' for %s: must be child of '%s'",
                            second_str, id_str, oneof_str);
                    ecs_os_free(second_str);
                    ecs_os_free(oneof_str);
                    ecs_os_free(id_str);
                    return -1;
                }
            }
        }
    }

    if (term->src.trav) {
        if (!ecs_has_id(world, term->src.trav, EcsAcyclic)) {
            char *r_str = ecs_get_fullpath(world, term->src.trav);
            flecs_filter_error(ctx, 
                "cannot traverse non-Acyclic relationship '%s'", r_str);
            ecs_os_free(r_str);
            return -1;
        }
    }

    return 0;
}

static
int flecs_term_finalize(
    const ecs_world_t *world,
    ecs_term_t *term,
    ecs_filter_finalize_ctx_t *ctx)
{
    ctx->term = term;

    ecs_term_id_t *src = &term->src;
    ecs_term_id_t *first = &term->first;
    ecs_term_id_t *second = &term->second;
    ecs_flags32_t first_flags = first->flags;
    ecs_flags32_t src_flags = src->flags;

    if (term->id) {
        if (flecs_term_populate_from_id(world, term, ctx)) {
            return -1;
        }
    }

    if (flecs_term_ids_finalize(world, term, ctx)) {
        return -1;
    }

    /* If EcsVariable is used by itself, assign to predicate (singleton) */
    if ((src->id == EcsVariable) && (src->flags & EcsIsVariable)) {
        src->id = first->id;
        src->flags &= ~(EcsIsVariable | EcsIsEntity);
        src->flags |= first->flags & (EcsIsVariable | EcsIsEntity);
    }
    if ((second->id == EcsVariable) && (second->flags & EcsIsVariable)) {
        second->id = first->id;
        second->flags &= ~(EcsIsVariable | EcsIsEntity);
        second->flags |= first->flags & (EcsIsVariable | EcsIsEntity);
    }

    if (!term->id) {
        if (flecs_term_populate_id(term, ctx)) {
            return -1;
        }
    }

    ecs_entity_t first_id = 0;
    if (term->first.flags & EcsIsEntity) {
        first_id = term->first.id;
    }

    /* If component id is final, don't attempt component inheritance */
    if (first_id) {
        if (ecs_has_id(world, first_id, EcsFinal)) {
            if (first_flags & EcsDown) {
                flecs_filter_error(ctx, "final id cannot be traversed down");
                return -1;
            }
            first->flags &= ~EcsDown;
            first->trav = 0;
        }
        if (ecs_has_id(world, first_id, EcsDontInherit)) {
            if (src_flags & (EcsUp | EcsDown)) {
                flecs_filter_error(ctx, 
                    "traversing not allowed for id that can't be inherited");
                return -1;
            }
            src->flags &= ~(EcsUp | EcsDown);
            src->trav = 0;
        }
    }

    if (first->id == EcsVariable) {
        flecs_filter_error(ctx, "invalid $ for term.first");
        return -1;
    }

    if (term->role == ECS_AND || term->role == ECS_OR || term->role == ECS_NOT){
        if (term->inout != EcsInOutDefault && term->inout != EcsInOutNone) {
            flecs_filter_error(ctx, "AND/OR terms must be filters");
            return -1;
        }

        term->inout = EcsInOutNone;

        /* Translate role to operator */
        if (term->role == ECS_AND) {
            term->oper = EcsAndFrom;
        } else
        if (term->role == ECS_OR) {
            term->oper = EcsOrFrom;
        } else
        if (term->role == ECS_NOT) {
            term->oper = EcsNotFrom;
        }

        /* Zero out role & strip from id */
        term->id &= ECS_COMPONENT_MASK;
        term->role = 0;
    }

    if (flecs_term_verify(world, term, ctx)) {
        return -1;
    }

    return 0;
}

ecs_id_t flecs_to_public_id(
    ecs_id_t id)
{
    if (ECS_PAIR_FIRST(id) == EcsUnion) {
        return ecs_pair(ECS_PAIR_SECOND(id), EcsWildcard);
    } else {
        return id;
    }
}

ecs_id_t flecs_from_public_id(
    ecs_world_t *world,
    ecs_id_t id)
{
    if (ECS_HAS_ROLE(id, PAIR)) {
        ecs_entity_t first = ECS_PAIR_FIRST(id);
        ecs_id_record_t *idr = flecs_id_record_ensure(world, 
            ecs_pair(first, EcsWildcard));
        if (idr->flags & EcsIdUnion) {
            return ecs_pair(EcsUnion, first);
        }
    }

    return id;
}

bool ecs_identifier_is_0(
    const char *id)
{
    return id[0] == '0' && !id[1];
}

bool ecs_id_match(
    ecs_id_t id,
    ecs_id_t pattern)
{
    if (id == pattern) {
        return true;
    }

    if (ECS_HAS_ROLE(pattern, PAIR)) {
        if (!ECS_HAS_ROLE(id, PAIR)) {
            return false;
        }

        ecs_entity_t id_rel = ECS_PAIR_FIRST(id);
        ecs_entity_t id_obj = ECS_PAIR_SECOND(id);
        ecs_entity_t pattern_rel = ECS_PAIR_FIRST(pattern);
        ecs_entity_t pattern_obj = ECS_PAIR_SECOND(pattern);

        ecs_check(id_rel != 0, ECS_INVALID_PARAMETER, NULL);
        ecs_check(id_obj != 0, ECS_INVALID_PARAMETER, NULL);

        ecs_check(pattern_rel != 0, ECS_INVALID_PARAMETER, NULL);
        ecs_check(pattern_obj != 0, ECS_INVALID_PARAMETER, NULL);
        
        if (pattern_rel == EcsWildcard) {
            if (pattern_obj == EcsWildcard || pattern_obj == id_obj) {
                return true;
            }
        } else if (pattern_obj == EcsWildcard) {
            if (pattern_rel == id_rel) {
                return true;
            }
        }
    } else {
        if ((id & ECS_ROLE_MASK) != (pattern & ECS_ROLE_MASK)) {
            return false;
        }

        if ((ECS_COMPONENT_MASK & pattern) == EcsWildcard) {
            return true;
        }
    }

error:
    return false;
}

bool ecs_id_is_pair(
    ecs_id_t id)
{
    return ECS_HAS_ROLE(id, PAIR);
}

bool ecs_id_is_wildcard(
    ecs_id_t id)
{
    if ((id == EcsWildcard) || (id == EcsAny)) {
        return true;
    }

    bool is_pair = ECS_HAS_ROLE(id, PAIR);
    if (!is_pair) {
        return false;
    }

    ecs_entity_t first = ECS_PAIR_FIRST(id);
    ecs_entity_t second = ECS_PAIR_SECOND(id);

    return (first == EcsWildcard) || (second == EcsWildcard) ||
           (first == EcsAny) || (second == EcsAny);
}

bool ecs_id_is_valid(
    const ecs_world_t *world,
    ecs_id_t id)
{
    if (!id) {
        return false;
    }
    if (ecs_id_is_wildcard(id)) {
        return false;
    }

    world = ecs_get_world(world);
    const ecs_id_record_t *idr = flecs_id_record_get(world, id);
    if (idr && idr->flags & EcsIdMarkedForDelete) {
        return false;
    }

    if (ECS_HAS_ROLE(id, PAIR)) {
        if (!ECS_PAIR_FIRST(id)) {
            return false;
        }
        if (!ECS_PAIR_SECOND(id)) {
            return false;
        }
    } else if (id & ECS_ROLE_MASK) {
        if (!ecs_is_valid(world, id & ECS_COMPONENT_MASK)) {
            return false;
        }
    }

    return true;
}

ecs_flags32_t ecs_id_get_flags(
    const ecs_world_t *world,
    ecs_id_t id)
{
    ecs_id_record_t *idr = flecs_id_record_get(world, id);
    if (idr) {
        return idr->flags;
    } else {
        return 0;
    }
}

bool ecs_term_id_is_set(
    const ecs_term_id_t *id)
{
    return id->id != 0 || id->name != NULL || id->flags & EcsIsEntity;
}

bool ecs_term_is_initialized(
    const ecs_term_t *term)
{
    return term->id != 0 || ecs_term_id_is_set(&term->first);
}

bool ecs_term_match_this(
    const ecs_term_t *term)
{
    return (term->src.flags & EcsIsVariable) && (term->src.id == EcsThis);
}

bool ecs_term_match_0(
    const ecs_term_t *term)
{
    return (term->src.id == 0) && (term->src.flags & EcsIsEntity);
}

int ecs_term_finalize(
    const ecs_world_t *world,
    ecs_term_t *term)
{
    ecs_filter_finalize_ctx_t ctx = {0};
    ctx.world = world;
    ctx.term = term;
    return flecs_term_finalize(world, term, &ctx);
}

ecs_term_t ecs_term_copy(
    const ecs_term_t *src)
{
    ecs_term_t dst = *src;
    dst.name = ecs_os_strdup(src->name);
    dst.first.name = ecs_os_strdup(src->first.name);
    dst.src.name = ecs_os_strdup(src->src.name);
    dst.second.name = ecs_os_strdup(src->second.name);
    return dst;
}

ecs_term_t ecs_term_move(
    ecs_term_t *src)
{
    if (src->move) {
        ecs_term_t dst = *src;
        src->name = NULL;
        src->first.name = NULL;
        src->src.name = NULL;
        src->second.name = NULL;
        dst.move = false;
        return dst;
    } else {
        ecs_term_t dst = ecs_term_copy(src);
        dst.move = false;
        return dst;
    }
}

void ecs_term_fini(
    ecs_term_t *term)
{
    ecs_os_free(term->first.name);
    ecs_os_free(term->src.name);
    ecs_os_free(term->second.name);
    ecs_os_free(term->name);

    term->first.name = NULL;
    term->src.name = NULL;
    term->second.name = NULL;
    term->name = NULL;
}

int ecs_filter_finalize(
    const ecs_world_t *world,
    ecs_filter_t *f)
{
    int32_t i, term_count = f->term_count, actual_count = 0;
    ecs_term_t *terms = f->terms;
    bool is_or = false, prev_or = false;
    ecs_flags32_t prev_src_flags = 0;
    ecs_entity_t prev_src_id = 0;
    int32_t filter_terms = 0;

    ecs_filter_finalize_ctx_t ctx = {0};
    ctx.world = world;
    ctx.filter = f;

    f->flags |= EcsFilterMatchOnlyThis;
    for (i = 0; i < term_count; i ++) {
        ecs_term_t *term = &terms[i];
        ctx.term_index = i;
        if (flecs_term_finalize(world, term, &ctx)) {
            return -1;
        }

        is_or = term->oper == EcsOr;
        actual_count += !(is_or && prev_or);
        term->index = actual_count - 1;

        if (prev_or && is_or) {
            if (prev_src_flags != term->src.flags) {
                flecs_filter_error(&ctx, "mismatching src.flags for OR terms");
                return -1;
            }
            if (prev_src_id != term->src.id) {
                flecs_filter_error(&ctx, "mismatching src.id for OR terms");
                return -1;
            }
        }

        prev_src_flags = term->src.flags;
        prev_src_id = term->src.id;
        prev_or = is_or;

        if (ecs_term_match_this(term)) {
            ECS_BIT_SET(f->flags, EcsFilterMatchThis);
        } else {
            ECS_BIT_CLEAR(f->flags, EcsFilterMatchOnlyThis);
        }

        if (term->id == EcsPrefab) {
            ECS_BIT_SET(f->flags, EcsFilterMatchPrefab);
        }
        if (term->id == EcsDisabled) {
            ECS_BIT_SET(f->flags, EcsFilterMatchDisabled);
        }

        if (ECS_BIT_IS_SET(f->flags, EcsFilterIsFilter)) {
            term->inout = EcsInOutNone;
        }

        if (term->inout == EcsInOutNone) {
            filter_terms ++;
        }

        if (term->oper != EcsNot || !ecs_term_match_this(term)) {
            ECS_BIT_CLEAR(f->flags, EcsFilterMatchAnything);
        }
    }

    f->term_count_actual = actual_count;

    if (filter_terms == term_count) {
        ECS_BIT_SET(f->flags, EcsFilterIsFilter);
    }

    return 0;
}

/* Implementation for iterable mixin */
static
void flecs_filter_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_filter_t);

    if (filter) {
        iter[1] = ecs_filter_iter(world, (ecs_filter_t*)poly);
        iter[0] = ecs_term_chain_iter(&iter[1], filter);
    } else {
        iter[0] = ecs_filter_iter(world, (ecs_filter_t*)poly);
    }
}

ecs_filter_t* ecs_filter_init(
    const ecs_world_t *stage,
    const ecs_filter_desc_t *desc)    
{
    ecs_check(stage != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(desc != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(desc->_canary == 0, ECS_INVALID_PARAMETER, NULL);
    const ecs_world_t *world = ecs_get_world(stage);

    ecs_filter_t *f = desc->storage;
    int32_t i, term_count = desc->terms_buffer_count, storage_count = 0, expr_count = 0;
    const ecs_term_t *terms = desc->terms_buffer;
    ecs_term_t *storage_terms = NULL, *expr_terms = NULL;

    if (f) {
        ecs_check(f->hdr.magic == ecs_filter_t_magic, 
            ECS_INVALID_PARAMETER, NULL);
        storage_count = f->term_count;
        storage_terms = f->terms;
        ecs_poly_init(f, ecs_filter_t);
    } else {
        f = ecs_poly_new(ecs_filter_t);
        f->owned = true;
    }
    if (!storage_terms) {
        f->terms_owned = true;
    }

    ECS_BIT_COND(f->flags, EcsFilterIsInstanced, desc->instanced);
    ECS_BIT_SET(f->flags, EcsFilterMatchAnything);
    f->flags |= desc->flags;

    /* If terms_buffer was not set, count number of initialized terms in
     * static desc::terms array */
    if (!terms) {
        ecs_check(term_count == 0, ECS_INVALID_PARAMETER, NULL);
        terms = desc->terms;
        for (i = 0; i < ECS_TERM_DESC_CACHE_SIZE; i ++) {
            if (!ecs_term_is_initialized(&terms[i])) {
                break;
            }
            term_count ++;
        }
    } else {
        ecs_check(term_count != 0, ECS_INVALID_PARAMETER, NULL);
    }

    /* If expr is set, parse query expression */
    const char *name = desc->name, *expr = desc->expr;
    if (expr) {
#ifdef FLECS_PARSER
        const char *ptr = desc->expr;
        ecs_term_t term = {0};
        int32_t expr_size = 0;
        while (ptr[0] && (ptr = ecs_parse_term(world, name, expr, ptr, &term))){
            if (!ecs_term_is_initialized(&term)) {
                break;
            }

            if (expr_count == expr_size) {
                expr_size = expr_size ? expr_size * 2 : 8;
                expr_terms = ecs_os_realloc_n(expr_terms, ecs_term_t, expr_size);
            }

            expr_terms[expr_count ++] = term;
            if (ptr[0] == '\n') {
                break;
            }
        }

        if (!ptr) {
            /* Set terms in filter object to make sur they get cleaned up */
            f->terms = expr_terms;
            f->term_count = expr_count;
            f->terms_owned = true;
            goto error;
        }
#else
        (void)expr;
        ecs_abort(ECS_UNSUPPORTED, "parser addon is not available");
#endif
    }

    /* If storage is provided, make sure it's large enough */
    ecs_check(!storage_terms || storage_count >= (term_count + expr_count),
        ECS_INVALID_PARAMETER, NULL);

    if (term_count || expr_count) {
        /* If no storage is provided, create it */
        if (!storage_terms) {
            f->terms = ecs_os_calloc_n(ecs_term_t, term_count + expr_count);
            f->term_count = term_count + expr_count;
            ecs_assert(f->terms_owned == true, ECS_INTERNAL_ERROR, NULL);
        } else {
            f->terms = storage_terms;
            f->term_count = storage_count;
        }

        /* Copy terms to filter storage */
        for (i = 0; i < term_count; i ++) {
            f->terms[i] = ecs_term_copy(&terms[i]);
            /* Allow freeing resources from expr parser during finalization */
            f->terms[i].move = true;
        }

        /* Move expr terms to filter storage */
        for (i = 0; i < expr_count; i ++) {
            f->terms[i + term_count] = ecs_term_move(&expr_terms[i]);
            /* Allow freeing resources from expr parser during finalization */
            f->terms[i + term_count].move = true;
        }
        ecs_os_free(expr_terms);
    }

    /* Ensure all fields are consistent and properly filled out */
    if (ecs_filter_finalize(world, f)) {
        goto error;
    }

    /* Any allocated resources remaining in terms are now owned by filter */
    for (i = 0; i < f->term_count; i ++) {
        f->terms[i].move = false;
    }

    f->name = ecs_os_strdup(name);
    f->variable_names[0] = (char*)".";
    f->iterable.init = flecs_filter_iter_init;

    return f;
error:
    ecs_filter_fini(f);
    return NULL;
}

void ecs_filter_copy(
    ecs_filter_t *dst,
    const ecs_filter_t *src)   
{
    if (src == dst) {
        return;
    }

    if (src) {
        *dst = *src;

        int32_t i, term_count = src->term_count;
        dst->terms = ecs_os_malloc_n(ecs_term_t, term_count);
        dst->terms_owned = true;

        for (i = 0; i < term_count; i ++) {
            dst->terms[i] = ecs_term_copy(&src->terms[i]);
        }
    } else {
        ecs_os_memset_t(dst, 0, ecs_filter_t);
    }
}

void ecs_filter_move(
    ecs_filter_t *dst,
    ecs_filter_t *src)   
{
    if (src == dst) {
        return;
    }

    if (src) {
        *dst = *src;
        if (src->terms_owned) {
            dst->terms = src->terms;
            dst->terms_owned = true;
        } else {
            ecs_filter_copy(dst, src);
        }
        src->terms = NULL;
        src->term_count = 0;
    } else {
        ecs_os_memset_t(dst, 0, ecs_filter_t);
    }
}

void ecs_filter_fini(
    ecs_filter_t *filter) 
{
    if (filter->terms) {
        int i, count = filter->term_count;
        for (i = 0; i < count; i ++) {
            ecs_term_fini(&filter->terms[i]);
        }

        if (filter->terms_owned) {
            ecs_os_free(filter->terms);
        }
    }

    ecs_os_free(filter->name);

    filter->terms = NULL;
    filter->name = NULL;

    if (filter->owned) {
        ecs_os_free(filter);
    }
}

static
void filter_str_add_id(
    const ecs_world_t *world,
    ecs_strbuf_t *buf,
    const ecs_term_id_t *id,
    bool is_subject,
    ecs_flags32_t default_traverse_flags)
{
    bool is_added = false;
    if (!is_subject || id->id != EcsThis) {
        if (id->flags & EcsIsVariable) {
            ecs_strbuf_appendstr(buf, "$");
        }
        if (id->id) {
            char *path = ecs_get_fullpath(world, id->id);
            ecs_strbuf_appendstr(buf, path);
            ecs_os_free(path);
        } else if (id->name) {
            ecs_strbuf_appendstr(buf, id->name);
        } else {
            ecs_strbuf_appendstr(buf, "0");
        }
        is_added = true;
    }

    ecs_flags32_t flags = id->flags;
    if (!(flags & EcsTraverseFlags)) {
        /* If flags haven't been set yet, initialize with defaults. This can
         * happen if an error is thrown while the term is being finalized */
        flags |= default_traverse_flags;
    }

    if ((flags & EcsTraverseFlags) != default_traverse_flags) {
        if (is_added) {
            ecs_strbuf_list_push(buf, ":", "|");
        } else {
            ecs_strbuf_list_push(buf, "", "|");
        }
        if (id->flags & EcsSelf) {
            ecs_strbuf_list_appendstr(buf, "self");
        }
        if (id->flags & EcsUp) {
            ecs_strbuf_list_appendstr(buf, "up");
        }
        if (id->flags & EcsDown) {
            ecs_strbuf_list_appendstr(buf, "down");
        }

        if (id->trav && (id->trav != EcsIsA)) {
            ecs_strbuf_list_push(buf, "(", "");

            char *rel_path = ecs_get_fullpath(world, id->trav);
            ecs_strbuf_appendstr(buf, rel_path);
            ecs_os_free(rel_path);

            ecs_strbuf_list_pop(buf, ")");
        }

        ecs_strbuf_list_pop(buf, "");
    }
}

static
void term_str_w_strbuf(
    const ecs_world_t *world,
    const ecs_term_t *term,
    ecs_strbuf_t *buf)
{
    const ecs_term_id_t *src = &term->src;
    const ecs_term_id_t *second = &term->second;

    uint8_t def_src_mask = EcsSelf|EcsUp;
    uint8_t def_first_mask = EcsSelf|EcsDown;
    uint8_t def_second_mask = EcsSelf;

    bool pred_set = ecs_term_id_is_set(&term->first);
    bool subj_set = !ecs_term_match_0(term);
    bool obj_set = ecs_term_id_is_set(second);

    if (term->first.flags & EcsIsEntity && term->first.id != 0) {
        if (ecs_has_id(world, term->first.id, EcsFinal)) {
            def_first_mask = EcsSelf;
        }
        if (ecs_has_id(world, term->first.id, EcsDontInherit)) {
            def_src_mask = EcsSelf;
        }
    }

    if (term->oper == EcsNot) {
        ecs_strbuf_appendstr(buf, "!");
    } else if (term->oper == EcsOptional) {
        ecs_strbuf_appendstr(buf, "?");
    }

    if (!subj_set) {
        filter_str_add_id(world, buf, &term->first, false, def_first_mask);
        ecs_strbuf_appendstr(buf, "()");
    } else if (ecs_term_match_this(term) && 
        (src->flags & EcsTraverseFlags) == def_src_mask)
    {
        if (pred_set) {
            if (obj_set) {
                ecs_strbuf_appendstr(buf, "(");
            }
            filter_str_add_id(world, buf, &term->first, false, def_first_mask);   
            if (obj_set) {
                ecs_strbuf_appendstr(buf, ",");
                filter_str_add_id(
                    world, buf, &term->second, false, def_second_mask);
                ecs_strbuf_appendstr(buf, ")");
            }
        } else if (term->id) {
            char *str = ecs_id_str(world, term->id);
            ecs_strbuf_appendstr(buf, str);
            ecs_os_free(str);
        }
    } else {
        if (term->role && term->role != ECS_PAIR) {
            ecs_strbuf_appendstr(buf, ecs_role_str(term->role));
            ecs_strbuf_appendch(buf, '|');
        }

        filter_str_add_id(world, buf, &term->first, false, def_first_mask);
        ecs_strbuf_appendstr(buf, "(");
        if (term->src.flags & EcsIsEntity && term->src.id == term->first.id) {
            ecs_strbuf_appendstr(buf, "$");
        } else {
            filter_str_add_id(world, buf, &term->src, true, def_src_mask);
        }
        if (obj_set) {
            ecs_strbuf_appendstr(buf, ",");
            filter_str_add_id(world, buf, &term->second, false, def_second_mask);
        }
        ecs_strbuf_appendstr(buf, ")");
    }
}

char* ecs_term_str(
    const ecs_world_t *world,
    const ecs_term_t *term)
{
    ecs_strbuf_t buf = ECS_STRBUF_INIT;
    term_str_w_strbuf(world, term, &buf);
    return ecs_strbuf_get(&buf);
}

static
char* flecs_filter_str(
    const ecs_world_t *world,
    const ecs_filter_t *filter,
    const ecs_filter_finalize_ctx_t *ctx,
    int32_t *term_start_out)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(filter != NULL, ECS_INVALID_PARAMETER, NULL);

    ecs_strbuf_t buf = ECS_STRBUF_INIT;
    ecs_term_t *terms = filter->terms;
    int32_t i, count = filter->term_count;
    int32_t or_count = 0;

    for (i = 0; i < count; i ++) {
        ecs_term_t *term = &terms[i];

        if (term_start_out && ctx) {
            if (ctx->term_index == i) {
                term_start_out[0] = ecs_strbuf_written(&buf);
                if (i) {
                    term_start_out[0] += 2; /* whitespace  + , */
                }
            }
        }

        if (i) {
            if (terms[i - 1].oper == EcsOr && term->oper == EcsOr) {
                ecs_strbuf_appendstr(&buf, " || ");
            } else {
                ecs_strbuf_appendstr(&buf, ", ");
            }
        }

        if (term->oper != EcsOr) {
            or_count = 0;
        }

        if (or_count < 1) {
            if (term->inout == EcsIn) {
                ecs_strbuf_appendstr(&buf, "[in] ");
            } else if (term->inout == EcsInOut) {
                ecs_strbuf_appendstr(&buf, "[inout] ");
            } else if (term->inout == EcsOut) {
                ecs_strbuf_appendstr(&buf, "[out] ");
            } else if (term->inout == EcsInOutNone) {
                ecs_strbuf_appendstr(&buf, "[none] ");
            }
        }

        if (term->oper == EcsOr) {
            or_count ++;
        }

        term_str_w_strbuf(world, term, &buf);
    }

    return ecs_strbuf_get(&buf);
error:
    return NULL;
}

char* ecs_filter_str(
    const ecs_world_t *world,
    const ecs_filter_t *filter)
{
    return flecs_filter_str(world, filter, NULL, NULL);
}

int32_t ecs_filter_find_this_var(
    const ecs_filter_t *filter)
{
    ecs_check(filter != NULL, ECS_INVALID_PARAMETER, NULL);
    
    if (ECS_BIT_IS_SET(filter->flags, EcsFilterMatchThis)) {
        /* Filters currently only support the This variable at index 0. Only
         * return 0 if filter actually has terms for the This variable. */
        return 0;
    }

error:
    return -1;
}

/* Check if the id is a pair that has Any as first or second element. Any 
 * pairs behave just like Wildcard pairs and reuses the same data structures,
 * with as only difference that the number of results returned for an Any pair
 * is never more than one. This function is used to tell the difference. */
static
bool is_any_pair(
    ecs_id_t id)
{
    if (!ECS_HAS_ROLE(id, PAIR)) {
        return false;
    }

    if (ECS_PAIR_FIRST(id) == EcsAny) {
        return true;
    }
    if (ECS_PAIR_SECOND(id) == EcsAny) {
        return true;
    }

    return false;
}

static
bool flecs_n_term_match_table(
    ecs_world_t *world,
    const ecs_term_t *term,
    const ecs_table_t *table,
    ecs_entity_t type_id,
    ecs_oper_kind_t oper,
    ecs_id_t *id_out,
    int32_t *column_out,
    ecs_entity_t *subject_out,
    int32_t *match_index_out,
    bool first,
    ecs_flags32_t iter_flags)
{
    (void)column_out;

    const ecs_type_t *type = ecs_get_type(world, type_id);
    ecs_assert(type != NULL, ECS_INTERNAL_ERROR, NULL);

    ecs_id_t *ids = type->array;
    int32_t i, count = type->count;
    ecs_term_t temp = *term;
    temp.oper = EcsAnd;

    for (i = 0; i < count; i ++) {
        ecs_id_t id = ids[i];
        if (ecs_id_get_flags(world, id) & EcsIdDontInherit) {
            continue;
        }
        bool result;
        if (ECS_HAS_ROLE(id, AND) || ECS_HAS_ROLE(id, OR)) {
            ecs_oper_kind_t id_oper = ECS_HAS_ROLE(id, AND)
                ? EcsAndFrom : ECS_HAS_ROLE(id, OR)
                    ? EcsOrFrom : 0;
            result = flecs_n_term_match_table(world, term, table, 
                id & ECS_COMPONENT_MASK, id_oper, id_out, column_out, 
                subject_out, match_index_out, first, iter_flags);
        } else {
            temp.id = id;
            result = flecs_term_match_table(world, &temp, table, id_out, 
                0, subject_out, match_index_out, first, iter_flags);
        }
        if (!result && oper == EcsAndFrom) {
            return false;
        } else
        if (result && oper == EcsOrFrom) {
            return true;
        }
    }

    if (oper == EcsAndFrom) {
        if (id_out) {
            id_out[0] = type_id;
        }
        return true;
    } else
    if (oper == EcsOrFrom) {
        return false;
    }

    return false;
}

bool flecs_term_match_table(
    ecs_world_t *world,
    const ecs_term_t *term,
    const ecs_table_t *table,
    ecs_id_t *id_out,
    int32_t *column_out,
    ecs_entity_t *subject_out,
    int32_t *match_index_out,
    bool first,
    ecs_flags32_t iter_flags)
{
    const ecs_term_id_t *src = &term->src;
    ecs_oper_kind_t oper = term->oper;
    const ecs_table_t *match_table = table;
    ecs_id_t id = term->id;

    ecs_entity_t src_id = src->id;
    if (ecs_term_match_0(term)) {
        if (id_out) {
            id_out[0] = id; /* If no entity is matched, just set id */
        }
        return true;
    }

    if (oper == EcsAndFrom || oper == EcsOrFrom) {
        return flecs_n_term_match_table(world, term, table, term->id, 
            term->oper, id_out, column_out, subject_out, match_index_out, first, 
            iter_flags);
    }

    /* If source is not This, search in table of source */
    if (!ecs_term_match_this(term)) {
        if (iter_flags & EcsIterEntityOptional) {
            /* Treat entity terms as optional */
            oper = EcsOptional;
        }

        match_table = ecs_get_table(world, src_id);
        if (match_table) {
        } else if (oper != EcsOptional) {
            return false;
        }
    } else {
        /* If filter contains This terms, a table must be provided */
        ecs_assert(table != NULL, ECS_INTERNAL_ERROR, NULL);
    }

    if (!match_table) {
        return false;
    }

    ecs_entity_t source = 0;

    /* If first = false, we're searching from an offset. This supports returning
     * multiple results when using wildcard filters. */
    int32_t column = 0;
    if (!first && column_out && column_out[0] != 0) {
        column = column_out[0];
        if (column < 0) {
            /* In case column is not from This, flip sign */
            column = -column;
        }

        /* Remove base 1 offset */
        column --;
    }

    /* Find location, source and id of match in table type */
    ecs_table_record_t *tr = 0;
    bool is_any = is_any_pair(id);

    column = ecs_search_relation(world, match_table,
        column, id, src->trav, src->flags, &source, id_out, &tr);

    if (tr && match_index_out) {
        if (!is_any) {
            match_index_out[0] = tr->count;
        } else {
            match_index_out[0] = 1;
        }
    }

    bool result = column != -1;

    if (oper == EcsNot) {
        if (match_index_out) {
            match_index_out[0] = 1;
        }
        result = !result;
    }

    if (oper == EcsOptional) {
        result = true;
    }

    if (!result) {
        if (iter_flags & EcsFilterPopulate) {
            column = 0;
        } else {
            return false;
        }
    }

    if (!ecs_term_match_this(term)) {
        if (!source) {
            source = src_id;
        }
    }

    if (id_out && column < 0) {
        id_out[0] = id;
    }

    if (column_out) {
        if (column >= 0) {
            column ++;
            if (source != 0) {
                column *= -1; 
            }
            column_out[0] = column;
        } else {
            column_out[0] = 0;
        }
    }

    if (subject_out) {
        subject_out[0] = source;
    }

    return result;
}

bool flecs_filter_match_table(
    ecs_world_t *world,
    const ecs_filter_t *filter,
    const ecs_table_t *table,
    ecs_id_t *ids,
    int32_t *columns,
    ecs_entity_t *sources,
    int32_t *match_indices,
    int32_t *matches_left,
    bool first,
    int32_t skip_term,
    ecs_flags32_t iter_flags)
{
    ecs_term_t *terms = filter->terms;
    int32_t i, count = filter->term_count;

    bool is_or = false;
    bool or_result = false;
    int32_t match_count = 1;
    if (matches_left) {
        match_count = *matches_left;
    }

    for (i = 0; i < count; i ++) {
        if (i == skip_term) {
            continue;
        }

        ecs_term_t *term = &terms[i];
        ecs_term_id_t *src = &term->src;
        ecs_oper_kind_t oper = term->oper;
        const ecs_table_t *match_table = table;
        int32_t t_i = term->index;

        if (!is_or && oper == EcsOr) {
            is_or = true;
            or_result = false;
        } else if (is_or && oper != EcsOr) {
            if (!or_result) {
                return false;
            }

            is_or = false;
        }

        ecs_entity_t src_id = src->id;
        if (!src_id) {
            if (ids) {
                ids[t_i] = term->id;
            }
            continue;
        }

        if (!ecs_term_match_this(term)) {
            match_table = ecs_get_table(world, src_id);
        } else {
            if (ECS_BIT_IS_SET(iter_flags, EcsIterIgnoreThis)) {
                or_result = true;
                continue;
            }
            
            /* If filter contains This terms, table must be provided */
            ecs_assert(table != NULL, ECS_INTERNAL_ERROR, NULL);
        }

        int32_t match_index = 0;

        bool result = flecs_term_match_table(world, term, match_table,
            ids ? &ids[t_i] : NULL, 
            columns ? &columns[t_i] : NULL, 
            sources ? &sources[t_i] : NULL, 
            &match_index,
            first,
            iter_flags);

        if (is_or) {
            or_result |= result;
            if (result) {
                /* If Or term matched, skip following Or terms */
                for (; i < count && terms[i].oper == EcsOr; i ++) { }
                i -- ;
            }
        } else if (!result) {
            return false;
        }

        if (first && match_index) {
            match_count *= match_index;
        }
        if (match_indices) {
            match_indices[t_i] = match_index;
        }
    }

    if (matches_left) {
        *matches_left = match_count;
    }

    return !is_or || or_result;
}

static
void term_iter_init_no_data(
    ecs_term_iter_t *iter)
{
    iter->term = (ecs_term_t){ .index = -1 };
    iter->self_index = NULL;
    iter->index = 0;
}

static
void term_iter_init_w_idr(
    ecs_term_iter_t *iter, 
    ecs_id_record_t *idr, 
    bool empty_tables)
{
    if (idr) {
        if (empty_tables) {
            flecs_table_cache_all_iter(&idr->cache, &iter->it);
        } else {
            flecs_table_cache_iter(&idr->cache, &iter->it);
        }
    } else {
        term_iter_init_no_data(iter);
    }

    iter->index = 0;
    iter->empty_tables = empty_tables;
}

static
void term_iter_init_wildcard(
    const ecs_world_t *world,
    ecs_term_iter_t *iter,
    bool empty_tables)
{
    iter->term = (ecs_term_t){ .index = -1 };
    iter->self_index = flecs_id_record_get(world, EcsAny);
    ecs_id_record_t *idr = iter->cur = iter->self_index;
    term_iter_init_w_idr(iter, idr, empty_tables);
}

static
void term_iter_init(
    const ecs_world_t *world,
    ecs_term_t *term,
    ecs_term_iter_t *iter,
    bool empty_tables)
{    
    const ecs_term_id_t *src = &term->src;

    iter->term = *term;

    if (src->flags & EcsSelf) {
        iter->self_index = flecs_query_id_record_get(world, term->id);
    }

    if (src->flags & EcsUp) {
        iter->set_index = flecs_id_record_get(world, 
            ecs_pair(src->trav, EcsWildcard));
    }

    ecs_id_record_t *idr;
    if (iter->self_index) {
        idr = iter->cur = iter->self_index;
    } else {
        idr = iter->cur = iter->set_index;
    }

    term_iter_init_w_idr(iter, idr, empty_tables);
}

ecs_iter_t ecs_term_iter(
    const ecs_world_t *stage,
    ecs_term_t *term)
{
    ecs_check(stage != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(term != NULL, ECS_INVALID_PARAMETER, NULL);

    const ecs_world_t *world = ecs_get_world(stage);

    flecs_process_pending_tables(world);

    if (ecs_term_finalize(world, term)) {
        ecs_throw(ECS_INVALID_PARAMETER, NULL);
    }

    ecs_iter_t it = {
        .real_world = (ecs_world_t*)world,
        .world = (ecs_world_t*)stage,
        .term_count = 1,
        .next = ecs_term_next
    };

    /* Term iter populates the iterator with arrays from its own cache, ensure 
     * they don't get overwritten by flecs_iter_validate.
     *
     * Note: the reason the term iterator doesn't use the iterator cache itself
     * (which could easily accomodate a single term) is that the filter iterator
     * is built on top of the term iterator. The private cache of the term
     * iterator keeps the filter iterator code simple, as it doesn't need to
     * worry about the term iter overwriting the iterator fields. */
    flecs_iter_init(&it, 0);

    term_iter_init(world, term, &it.priv.iter.term, false);

    return it;
error:
    return (ecs_iter_t){ 0 };
}

ecs_iter_t ecs_term_chain_iter(
    const ecs_iter_t *chain_it,
    ecs_term_t *term)
{
    ecs_check(chain_it != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(term != NULL, ECS_INVALID_PARAMETER, NULL);

    ecs_world_t *world = chain_it->real_world;
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);

    if (ecs_term_finalize(world, term)) {
        ecs_throw(ECS_INVALID_PARAMETER, NULL);
    }

    ecs_iter_t it = {
        .real_world = (ecs_world_t*)world,
        .world = chain_it->world,
        .terms = term,
        .term_count = 1,
        .chain_it = (ecs_iter_t*)chain_it,
        .next = ecs_term_next
    };

    flecs_iter_init(&it, flecs_iter_cache_all);

    term_iter_init(world, term, &it.priv.iter.term, false);

    return it;
error:
    return (ecs_iter_t){ 0 };
}

static
const ecs_table_record_t *flecs_term_iter_next_table(
    ecs_term_iter_t *iter)
{
    ecs_id_record_t *idr = iter->cur;
    if (!idr) {
        return NULL;
    }

    return flecs_table_cache_next(&iter->it, ecs_table_record_t);
}

static
bool flecs_term_iter_find_superset(
    ecs_world_t *world, 
    ecs_table_t *table, 
    ecs_term_t *term, 
    ecs_entity_t *source, 
    ecs_id_t *id, 
    int32_t *column)
{
    ecs_term_id_t *src = &term->src;

    /* Test if following the relationship finds the id */
    int32_t index = ecs_search_relation(world, table, 0, 
        term->id, src->trav, src->flags, source, id, 0);

    if (index == -1) {
        *source = 0;
        return false;
    }

    ecs_assert(*source != 0, ECS_INTERNAL_ERROR, NULL);

    *column = (index + 1) * -1;

    return true;
}

static
bool flecs_term_iter_next(
    ecs_world_t *world,
    ecs_term_iter_t *iter,
    bool match_prefab,
    bool match_disabled)
{
    ecs_table_t *table = iter->table;
    ecs_entity_t source = 0;
    const ecs_table_record_t *tr;
    ecs_term_t *term = &iter->term;

    do {
        if (table) {
            iter->cur_match ++;
            if (iter->cur_match >= iter->match_count) {
                table = NULL;
            } else {
                iter->last_column = ecs_search_offset(
                    world, table, iter->last_column + 1, term->id, 0);
                iter->column = iter->last_column + 1;
                if (iter->last_column >= 0) {
                    iter->id = table->type.array[iter->last_column];
                }
            }
        }

        if (!table) {
            if (!(tr = flecs_term_iter_next_table(iter))) {
                if (iter->cur != iter->set_index && iter->set_index != NULL) {
                    iter->cur = iter->set_index;
                    if (iter->empty_tables) {
                        flecs_table_cache_all_iter(
                            &iter->set_index->cache, &iter->it);
                    } else {
                        flecs_table_cache_iter(
                            &iter->set_index->cache, &iter->it);
                    }
                    iter->index = 0;
                    tr = flecs_term_iter_next_table(iter);
                }

                if (!tr) {
                    return false;
                }
            }

            table = tr->hdr.table;

            if (!match_prefab && (table->flags & EcsTableIsPrefab)) {
                continue;
            }

            if (!match_disabled && (table->flags & EcsTableIsDisabled)) {
                continue;
            }

            iter->table = table;
            iter->match_count = tr->count;
            if (is_any_pair(term->id)) {
                iter->match_count = 1;
            }

            iter->cur_match = 0;
            iter->last_column = tr->column;
            iter->column = tr->column + 1;
            iter->id = flecs_to_public_id(table->type.array[tr->column]);
        }

        if (iter->cur == iter->set_index) {
            if (iter->self_index) {
                if (flecs_id_record_get_table(iter->self_index, table) != NULL) {
                    /* If the table has the id itself and this term matched Self
                     * we already matched it */
                    continue;
                }
            }

            if (!flecs_term_iter_find_superset(
                world, table, term, &source, &iter->id, &iter->column)) 
            {
                continue;
            }

            /* The tr->count field refers to the number of relationship instances,
             * not to the number of matches. Superset terms can only yield a
             * single match. */
            iter->match_count = 1;
        }

        break;
    } while (true);

    iter->subject = source;

    return true;
}

static
bool flecs_term_iter_set_table(
    ecs_world_t *world,
    ecs_term_iter_t *iter,
    ecs_table_t *table)
{
    const ecs_table_record_t *tr = NULL;
    const ecs_id_record_t *idr = iter->self_index;
    if (idr) {
        tr = ecs_table_cache_get(&idr->cache, table);
        if (tr) {
            iter->match_count = tr->count;
            iter->last_column = tr->column;
            iter->column = tr->column + 1;
            iter->id = flecs_to_public_id(table->type.array[tr->column]);
        }
    }

    if (!tr) {
        idr = iter->set_index;
        if (idr) {
            tr = ecs_table_cache_get(&idr->cache, table);
            if (!flecs_term_iter_find_superset(world, table, &iter->term, 
                &iter->subject, &iter->id, &iter->column)) 
            {
                return false;
            }
            iter->match_count = 1;
        }
    }

    if (!tr) {
        return false;
    }

    /* Populate fields as usual */
    iter->table = table;
    iter->cur_match = 0;

    return true;
}

bool ecs_term_next(
    ecs_iter_t *it)
{
    ecs_check(it != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(it->next == ecs_term_next, ECS_INVALID_PARAMETER, NULL);

    flecs_iter_validate(it);

    ecs_term_iter_t *iter = &it->priv.iter.term;
    ecs_term_t *term = &iter->term;
    ecs_world_t *world = it->real_world;
    ecs_table_t *table;

    it->ids = &iter->id;
    it->sources = &iter->subject;
    it->columns = &iter->column;
    it->terms = &iter->term;
    it->sizes = &iter->size;

    if (term->inout != EcsInOutNone) {
        it->ptrs = &iter->ptr;
    } else {
        it->ptrs = NULL;
    }

    ecs_iter_t *chain_it = it->chain_it;
    if (chain_it) {
        ecs_iter_next_action_t next = chain_it->next;
        bool match;

        do {
            if (!next(chain_it)) {
                goto done;
            }

            table = chain_it->table;
            match = flecs_term_match_table(world, term, table,
                it->ids, it->columns, it->sources, it->match_indices, true,
                it->flags);
        } while (!match);
        goto yield;

    } else {
        if (!flecs_term_iter_next(world, iter, false, false)) {
            goto done;
        }

        table = iter->table;

        /* Source must either be 0 (EcsThis) or nonzero in case of substitution */
        ecs_assert(iter->subject || iter->cur != iter->set_index, 
            ECS_INTERNAL_ERROR, NULL);
        ecs_assert(iter->table != NULL, ECS_INTERNAL_ERROR, NULL);
    }

yield:
    flecs_iter_populate_data(world, it, table, 0, 0, it->ptrs, it->sizes);
    ECS_BIT_SET(it->flags, EcsIterIsValid);
    return true;
done:
error:
    return false;
}

static
void flecs_init_filter_iter(
    ecs_iter_t *it,
    const ecs_filter_t *filter)
{
    ecs_assert(filter != NULL, ECS_INTERNAL_ERROR, NULL);
    it->priv.iter.filter.filter = filter;
    it->term_count = filter->term_count_actual;
}

int32_t ecs_filter_pivot_term(
    const ecs_world_t *world,
    const ecs_filter_t *filter)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(filter != NULL, ECS_INVALID_PARAMETER, NULL);

    ecs_term_t *terms = filter->terms;
    int32_t i, term_count = filter->term_count;
    int32_t pivot_term = -1, min_count = -1, self_pivot_term = -1;

    for (i = 0; i < term_count; i ++) {
        ecs_term_t *term = &terms[i];
        ecs_id_t id = term->id;

        if (term->oper != EcsAnd) {
            continue;
        }

        if (!ecs_term_match_this(term)) {
            continue;
        }

        ecs_id_record_t *idr = flecs_query_id_record_get(world, id);
        if (!idr) {
            /* If one of the terms does not match with any data, iterator 
             * should not return anything */
            return -2; /* -2 indicates filter doesn't match anything */
        }

        int32_t table_count = flecs_table_cache_count(&idr->cache);
        if (min_count == -1 || table_count < min_count) {
            min_count = table_count;
            pivot_term = i;
            if ((term->src.flags & EcsTraverseFlags) == EcsSelf) {
                self_pivot_term = i;
            }
        }
    }

    if (self_pivot_term != -1) {
        pivot_term = self_pivot_term;
    }

    return pivot_term;
error:
    return -2;
}

ecs_iter_t flecs_filter_iter_w_flags(
    const ecs_world_t *stage,
    const ecs_filter_t *filter,
    ecs_flags32_t flags)
{
    ecs_check(stage != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(filter != NULL, ECS_INVALID_PARAMETER, NULL);
    const ecs_world_t *world = ecs_get_world(stage);
    
    flecs_process_pending_tables(world);

    ecs_iter_t it = {
        .real_world = (ecs_world_t*)world,
        .world = (ecs_world_t*)stage,
        .terms = filter ? filter->terms : NULL,
        .next = ecs_filter_next,
        .flags = flags
    };

    ecs_filter_iter_t *iter = &it.priv.iter.filter;
    iter->pivot_term = -1;

    flecs_init_filter_iter(&it, filter);
    ECS_BIT_COND(it.flags, EcsIterIsInstanced, 
        ECS_BIT_IS_SET(filter->flags, EcsFilterIsInstanced));

    /* Find term that represents smallest superset */
    if (ECS_BIT_IS_SET(flags, EcsIterIgnoreThis)) {
        term_iter_init_no_data(&iter->term_iter);
    } else if (ECS_BIT_IS_SET(filter->flags, EcsFilterMatchThis)) {
        ecs_term_t *terms = filter->terms;
        int32_t pivot_term = -1;
        ecs_check(terms != NULL, ECS_INVALID_PARAMETER, NULL);

        pivot_term = ecs_filter_pivot_term(world, filter);
        iter->kind = EcsIterEvalTables;
        iter->pivot_term = pivot_term;

        if (pivot_term == -2) {
            /* One or more terms have no matching results */
            term_iter_init_no_data(&iter->term_iter);
        } else if (pivot_term == -1) {
            /* No terms meet the criteria to be a pivot term, evaluate filter
             * against all tables */
            term_iter_init_wildcard(world, &iter->term_iter, 
                ECS_BIT_IS_SET(filter->flags, EcsFilterMatchEmptyTables));
        } else {
            ecs_assert(pivot_term >= 0, ECS_INTERNAL_ERROR, NULL);
            term_iter_init(world, &terms[pivot_term], &iter->term_iter,
                ECS_BIT_IS_SET(filter->flags, EcsFilterMatchEmptyTables));
        }
    } else {
        if (!ECS_BIT_IS_SET(filter->flags, EcsFilterMatchAnything)) {
            term_iter_init_no_data(&iter->term_iter);
        } else {
            iter->kind = EcsIterEvalNone;
        }
    }

    ECS_BIT_COND(it.flags, EcsIterIsFilter, 
        ECS_BIT_IS_SET(filter->flags, EcsFilterIsFilter));

    if (ECS_BIT_IS_SET(filter->flags, EcsFilterMatchThis)) {
        /* Make space for one variable if the filter has terms for This var */ 
        it.variable_count = 1;

        /* Set variable name array */
        it.variable_names = (char**)filter->variable_names;
    }

    flecs_iter_init(&it, flecs_iter_cache_all);

    return it;
error:
    return (ecs_iter_t){ 0 };
}

ecs_iter_t ecs_filter_iter(
    const ecs_world_t *stage,
    const ecs_filter_t *filter)
{
    return flecs_filter_iter_w_flags(stage, filter, 0);
}

ecs_iter_t ecs_filter_chain_iter(
    const ecs_iter_t *chain_it,
    const ecs_filter_t *filter)
{
    ecs_iter_t it = {
        .terms = filter->terms,
        .term_count = filter->term_count,
        .world = chain_it->world,
        .real_world = chain_it->real_world,
        .chain_it = (ecs_iter_t*)chain_it,
        .next = ecs_filter_next
    };

    flecs_iter_init(&it, flecs_iter_cache_all);
    ecs_filter_iter_t *iter = &it.priv.iter.filter;
    flecs_init_filter_iter(&it, filter);

    iter->kind = EcsIterEvalChain;

    return it;
}

bool ecs_filter_next(
    ecs_iter_t *it)
{
    ecs_check(it != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(it->next == ecs_filter_next, ECS_INVALID_PARAMETER, NULL);

    if (flecs_iter_next_row(it)) {
        return true;
    }

    return flecs_iter_next_instanced(it, ecs_filter_next_instanced(it));
error:
    return false;
}

bool ecs_filter_next_instanced(
    ecs_iter_t *it)
{
    ecs_check(it != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(it->next == ecs_filter_next, ECS_INVALID_PARAMETER, NULL);
    ecs_check(it->chain_it != it, ECS_INVALID_PARAMETER, NULL);

    ecs_filter_iter_t *iter = &it->priv.iter.filter;
    const ecs_filter_t *filter = iter->filter;
    ecs_world_t *world = it->real_world;
    ecs_table_t *table = NULL;
    bool match;

    flecs_iter_validate(it);

    ecs_iter_t *chain_it = it->chain_it;
    ecs_iter_kind_t kind = iter->kind;

    if (chain_it) {
        ecs_assert(kind == EcsIterEvalChain, ECS_INVALID_PARAMETER, NULL);
        
        ecs_iter_next_action_t next = chain_it->next;
        do {
            if (!next(chain_it)) {
                goto done;
            }

            table = chain_it->table;
            match = flecs_filter_match_table(world, filter, table,
                it->ids, it->columns, it->sources, it->match_indices, NULL, 
                true, -1, it->flags);
        } while (!match);

        goto yield;
    } else if (kind == EcsIterEvalTables || kind == EcsIterEvalCondition) {
        ecs_term_iter_t *term_iter = &iter->term_iter;
        ecs_term_t *term = &term_iter->term;
        int32_t pivot_term = iter->pivot_term;
        bool first;

        /* Check if the This variable has been set on the iterator. If set,
         * the filter should only be applied to the variable value */
        ecs_var_t *this_var = NULL;
        ecs_table_t *this_table = NULL;
        if (it->variable_count) {
            if (ecs_iter_var_is_constrained(it, 0)) {
                this_var = it->variables;
                this_table = this_var->range.table;

                /* If variable is constrained, make sure it's a value that's
                 * pointing to a table, as a filter can't iterate single
                 * entities (yet) */
                ecs_assert(this_table != NULL, ECS_INVALID_OPERATION, NULL);

                /* Can't set variable for filter that does not iterate tables */
                ecs_assert(kind == EcsIterEvalTables, 
                    ECS_INVALID_OPERATION, NULL);
            }
        }

        do {
            /* If there are no matches left for the previous table, this is the
             * first match of the next table. */
            first = iter->matches_left == 0;

            if (first) {
                if (kind != EcsIterEvalCondition) {
                    /* Check if this variable was constrained */
                    if (this_table != NULL) {                        
                        /* If this is the first match of a new result and the
                         * previous result was equal to the value of a 
                         * constrained var, there's nothing left to iterate */
                        if (it->table == this_table) {
                            goto done;
                        }

                        /* If table doesn't match term iterator, it doesn't
                         * match filter. */
                        if (!flecs_term_iter_set_table(
                            world, term_iter, this_table))
                        {
                            goto done;
                        }

                        /* But if it does, forward it to filter matching */
                        ecs_assert(term_iter->table == this_table,
                            ECS_INTERNAL_ERROR, NULL);

                    /* If This variable is not constrained, iterate as usual */
                    } else {
                        /* Find new match, starting with the leading term */
                        if (!flecs_term_iter_next(world, term_iter, 
                            ECS_BIT_IS_SET(filter->flags, 
                                EcsFilterMatchPrefab), 
                            ECS_BIT_IS_SET(filter->flags, 
                                EcsFilterMatchDisabled))) 
                        {
                            goto done;
                        }
                    }

                    ecs_assert(term_iter->match_count != 0, 
                        ECS_INTERNAL_ERROR, NULL);

                    if (pivot_term == -1) {
                        /* Without a pivot term, we're iterating all tables with
                         * a wildcard, so the match count is meaningless. */
                        term_iter->match_count = 1;
                    } else {
                        it->match_indices[pivot_term] = term_iter->match_count;
                    }

                    iter->matches_left = term_iter->match_count;

                    /* Filter iterator takes control over iterating all the
                     * permutations that match the wildcard. */
                    term_iter->match_count = 1;

                    table = term_iter->table;

                    if (pivot_term != -1) {
                        int32_t index = term->index;
                        it->ids[index] = term_iter->id;
                        it->sources[index] = term_iter->subject;
                        it->columns[index] = term_iter->column;
                    }
                } else {
                    /* Progress iterator to next match for table, if any */
                    table = it->table;
                    if (term_iter->index == 0) {
                        iter->matches_left = 1;
                        term_iter->index = 1; /* prevents looping again */
                    } else {
                        goto done;
                    }
                }

                /* Match the remainder of the terms */
                match = flecs_filter_match_table(world, filter, table,
                    it->ids, it->columns, it->sources,
                    it->match_indices, &iter->matches_left, first, 
                    pivot_term, it->flags);
                if (!match) {
                    it->table = table;
                    iter->matches_left = 0;
                    continue;
                }

                /* Table got matched, set This variable */
                if (table) {
                    ecs_assert(it->variable_count == 1, ECS_INTERNAL_ERROR, NULL);
                    ecs_assert(it->variables != NULL, ECS_INTERNAL_ERROR, NULL);
                    it->variables[0].range.table = table;
                }

                ecs_assert(iter->matches_left != 0, ECS_INTERNAL_ERROR, NULL);
            }

            /* If this is not the first result for the table, and the table
             * is matched more than once, iterate remaining matches */
            if (!first && (iter->matches_left > 0)) {
                table = it->table;

                /* Find first term that still has matches left */
                int32_t i, j, count = it->term_count;
                for (i = count - 1; i >= 0; i --) {
                    int32_t mi = -- it->match_indices[i];
                    if (mi) {
                        if (mi < 0) {
                            continue;
                        }
                        break;
                    }
                }

                /* If matches_left > 0 we should've found at least one match */
                ecs_assert(i >= 0, ECS_INTERNAL_ERROR, NULL);

                /* Progress first term to next match (must be at least one) */
                int32_t column = it->columns[i];
                if (column < 0) {
                    /* If this term was matched on a non-This entity, reconvert
                     * the column back to a positive value */
                    column = -column;
                }

                it->columns[i] = column + 1;
                flecs_term_match_table(world, &filter->terms[i], table, 
                    &it->ids[i], &it->columns[i], &it->sources[i],
                    &it->match_indices[i], false, it->flags);

                /* Reset remaining terms (if any) to first match */
                for (j = i + 1; j < count; j ++) {
                    flecs_term_match_table(world, &filter->terms[j], table, 
                        &it->ids[j], &it->columns[j], &it->sources[j], 
                        &it->match_indices[j], true, it->flags);
                }
            }

            match = iter->matches_left != 0;
            iter->matches_left --;

            ecs_assert(iter->matches_left >= 0, ECS_INTERNAL_ERROR, NULL);
        } while (!match);

        goto yield;
    }

done:
error:
    ecs_iter_fini(it);
    return false;

yield:
    it->offset = 0;
    flecs_iter_populate_data(world, it, table, 0, 0, it->ptrs, it->sizes);
    ECS_BIT_SET(it->flags, EcsIterIsValid);
    return true;    
}


static
int32_t type_search(
    const ecs_table_t *table,
    ecs_id_record_t *idr,
    ecs_id_t *ids,
    ecs_id_t *id_out,
    ecs_table_record_t **tr_out)
{    
    ecs_table_record_t *tr = ecs_table_cache_get(&idr->cache, table);
    if (tr) {
        int32_t r = tr->column;
        if (tr_out) tr_out[0] = tr;
        if (id_out) {
            id_out[0] = flecs_to_public_id(ids[r]);
        }
        return r;
    }

    return -1;
}

static
int32_t type_offset_search(
    int32_t offset,
    ecs_id_t id,
    ecs_id_t *ids,
    int32_t count,
    ecs_id_t *id_out)
{
    ecs_assert(ids != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(count > 0, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(offset > 0, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(id != 0, ECS_INVALID_PARAMETER, NULL);

    while (offset < count) {
        ecs_id_t type_id = ids[offset ++];
        if (ecs_id_match(type_id, id)) {
            if (id_out) {
                id_out[0] = flecs_to_public_id(type_id);
            }
            return offset - 1;
        }
    }

    return -1;
}

static
bool type_can_inherit_id(
    const ecs_world_t *world,
    const ecs_table_t *table,
    const ecs_id_record_t *idr,
    ecs_id_t id)
{
    if (idr->flags & EcsIdDontInherit) {
        return false;
    }
    if (idr->flags & EcsIdExclusive) {
        if (ECS_HAS_ROLE(id, PAIR)) {
            ecs_entity_t er = ECS_PAIR_FIRST(id);
            if (flecs_table_record_get(
                world, table, ecs_pair(er, EcsWildcard))) 
            {
                return false;
            }
        }
    }
    return true;
}

static
int32_t type_search_relation(
    const ecs_world_t *world,
    const ecs_table_t *table,
    int32_t offset,
    ecs_id_t id,
    ecs_id_record_t *idr,
    ecs_id_t rel,
    ecs_id_record_t *idr_r,
    bool self,
    ecs_entity_t *subject_out,
    ecs_id_t *id_out,
    ecs_table_record_t **tr_out)
{
    ecs_type_t type = table->type;
    ecs_id_t *ids = type.array;
    int32_t count = type.count;

    if (self) {
        if (offset) {
            int32_t r = type_offset_search(offset, id, ids, count, id_out);
            if (r != -1) {
                return r;
            }
        } else {
            int32_t r = type_search(table, idr, ids, id_out, tr_out);
            if (r != -1) {
                return r;
            }
        }
    }

    ecs_flags32_t flags = table->flags;
    if ((flags & EcsTableHasPairs) && rel) {
        bool is_a = rel == ecs_pair(EcsIsA, EcsWildcard);
        if (is_a) {
            if (!(flags & EcsTableHasIsA)) {
                return -1;
            }
            if (!type_can_inherit_id(world, table, idr, id)) {
                return -1;
            }
            idr_r = world->idr_isa_wildcard;
        }

        if (!idr_r) {
            idr_r = flecs_id_record_get(world, rel);
            if (!idr_r) {
                return -1;
            }
        }

        ecs_id_t id_r;
        int32_t r, r_column;
        if (offset) {
            r_column = type_offset_search(offset, rel, ids, count, &id_r);
        } else {
            r_column = type_search(table, idr_r, ids, &id_r, 0);
        }
        while (r_column != -1) {
            ecs_entity_t obj = ECS_PAIR_SECOND(id_r);
            ecs_assert(obj != 0, ECS_INTERNAL_ERROR, NULL);

            ecs_record_t *rec = flecs_entities_get_any(world, obj);
            ecs_assert(rec != NULL, ECS_INTERNAL_ERROR, NULL);

            ecs_table_t *obj_table = rec->table;
            if (obj_table) {
                ecs_assert(obj_table != table, ECS_CYCLE_DETECTED, NULL);
                
                r = type_search_relation(world, obj_table, 0, id, idr, 
                    rel, idr_r, true, subject_out, id_out, tr_out);
                if (r != -1) {
                    if (subject_out && !subject_out[0]) {
                        subject_out[0] = ecs_get_alive(world, obj);
                    }
                    return r_column;
                }

                if (!is_a) {
                    r = type_search_relation(world, obj_table, 0, id, idr, 
                        ecs_pair(EcsIsA, EcsWildcard), world->idr_isa_wildcard, 
                            true, subject_out, id_out, tr_out);
                    if (r != -1) {
                        if (subject_out && !subject_out[0]) {
                            subject_out[0] = ecs_get_alive(world, obj);
                        }
                        return r_column;
                    }
                }
            }

            r_column = type_offset_search(r_column + 1, rel, ids, count, &id_r);
        }
    }

    return -1;
}

int32_t ecs_search_relation(
    const ecs_world_t *world,
    const ecs_table_t *table,
    int32_t offset,
    ecs_id_t id,
    ecs_entity_t rel,
    ecs_flags32_t flags,
    ecs_entity_t *subject_out,
    ecs_id_t *id_out,
    struct ecs_table_record_t **tr_out)
{
    if (!table) return -1;

    ecs_poly_assert(world, ecs_world_t);
    ecs_assert(id != 0, ECS_INVALID_PARAMETER, NULL);

    flags = flags ? flags : (EcsSelf|EcsUp);

    if (!(flags & EcsUp)) {
        if (subject_out) subject_out[0] = 0;
        return ecs_search_offset(world, table, offset, id, id_out);
    }

    ecs_id_record_t *idr = flecs_query_id_record_get(world, id);
    if (!idr) {
        return -1;
    }

    int32_t result = type_search_relation(world, table, offset, id, idr, 
        ecs_pair(rel, EcsWildcard), NULL, flags & EcsSelf, subject_out, 
            id_out, tr_out);

    return result;
}

int32_t ecs_search(
    const ecs_world_t *world,
    const ecs_table_t *table,
    ecs_id_t id,
    ecs_id_t *id_out)
{
    if (!table) return -1;

    ecs_poly_assert(world, ecs_world_t);
    ecs_assert(id != 0, ECS_INVALID_PARAMETER, NULL);

    ecs_id_record_t *idr = flecs_query_id_record_get(world, id);
    if (!idr) {
        return -1;
    }

    ecs_type_t type = table->type;
    ecs_id_t *ids = type.array;
    return type_search(table, idr, ids, id_out, NULL);
}

int32_t ecs_search_offset(
    const ecs_world_t *world,
    const ecs_table_t *table,
    int32_t offset,
    ecs_id_t id,
    ecs_id_t *id_out)
{
    if (!offset) {
        return ecs_search(world, table, id, id_out);
    }

    if (!table) return -1;

    ecs_poly_assert(world, ecs_world_t);

    ecs_type_t type = table->type;
    ecs_id_t *ids = type.array;
    int32_t count = type.count;
    return type_offset_search(offset, id, ids, count, id_out);
}

static
int32_t flecs_relation_depth_walk(
    const ecs_world_t *world,
    ecs_id_record_t *idr,
    ecs_table_t *first,
    ecs_table_t *table)
{
    int32_t result = 0;

    const ecs_table_record_t *tr = flecs_id_record_get_table(idr, table);
    if (!tr) {
        return 0;
    }

    int32_t i = tr->column, end = i + tr->count;
    for (; i != end; i ++) {
        ecs_entity_t o = ecs_pair_second(world, table->type.array[i]);
        ecs_assert(o != 0, ECS_INTERNAL_ERROR, NULL);

        ecs_table_t *ot = ecs_get_table(world, o);
        if (!ot) {
            continue;
        }
        
        ecs_assert(ot != first, ECS_CYCLE_DETECTED, NULL);
        int32_t cur = flecs_relation_depth_walk(world, idr, first, ot);
        if (cur > result) {
            result = cur;
        }
    }
    
    return result + 1;
}

int32_t flecs_relation_depth(
    const ecs_world_t *world,
    ecs_entity_t r,
    ecs_table_t *table)
{
    ecs_id_record_t *idr = flecs_id_record_get(world, ecs_pair(r, EcsWildcard));
    if (!idr) {
        return 0;
    }
    return flecs_relation_depth_walk(world, idr, table, table);
}

#include <stddef.h>

static
bool flecs_multi_observer_invoke(ecs_iter_t *it) {
    ecs_observer_t *o = it->ctx;
    ecs_world_t *world = it->real_world;

    if (o->last_event_id[0] == world->event_id) {
        /* Already handled this event */
        return false;
    }

    o->last_event_id[0] = world->event_id;

    ecs_iter_t user_it = *it;
    user_it.term_count = o->filter.term_count_actual;
    user_it.terms = o->filter.terms;
    user_it.flags = 0;
    ECS_BIT_COND(user_it.flags, EcsIterIsFilter,    
        ECS_BIT_IS_SET(o->filter.flags, EcsFilterIsFilter));
    user_it.ids = NULL;
    user_it.columns = NULL;
    user_it.sources = NULL;
    user_it.sizes = NULL;
    user_it.ptrs = NULL;
    flecs_iter_init(&user_it, flecs_iter_cache_all);

    ecs_table_t *table = it->table;
    ecs_table_t *prev_table = it->other_table;
    int32_t pivot_term = it->term_index;
    ecs_term_t *term = &o->filter.terms[pivot_term];

    int32_t column = it->columns[0];
    if (term->oper == EcsNot) {
        table = it->other_table;
        prev_table = it->table;
    }

    if (!table) {
        table = &world->store.root;
    }
    if (!prev_table) {
        prev_table = &world->store.root;
    }

    if (column < 0) {
        column = -column;
    }

    user_it.columns[0] = 0;
    user_it.columns[pivot_term] = column;

    if (flecs_filter_match_table(world, &o->filter, table, user_it.ids, 
        user_it.columns, user_it.sources, NULL, NULL, false, -1, 
        user_it.flags))
    {
        /* Monitor observers only invoke when the filter matches for the first
         * time with an entity */
        if (o->is_monitor) {
            if (flecs_filter_match_table(world, &o->filter, prev_table, 
                NULL, NULL, NULL, NULL, NULL, true, -1, user_it.flags)) 
            {
                goto done;
            }
        }

        /* While filter matching needs to be reversed for a Not term, the
         * component data must be fetched from the table we got notified for.
         * Repeat the matching process for the non-matching table so we get the
         * correct column ids and sources, which we need for populate_data */
        if (term->oper == EcsNot) {
            flecs_filter_match_table(world, &o->filter, prev_table, user_it.ids, 
                user_it.columns, user_it.sources, NULL, NULL, false, -1, 
                user_it.flags | EcsFilterPopulate);
        }

        flecs_iter_populate_data(world, &user_it, it->table, it->offset, 
            it->count, user_it.ptrs, user_it.sizes);

        user_it.ids[it->term_index] = it->event_id;
        user_it.system = o->entity;
        user_it.term_index = it->term_index;
        user_it.ctx = o->ctx;
        user_it.binding_ctx = o->binding_ctx;
        user_it.term_count = o->filter.term_count_actual;
        flecs_iter_validate(&user_it);

        ecs_assert(o->callback != NULL, ECS_INVALID_PARAMETER, NULL);
        o->callback(&user_it);

        ecs_iter_fini(&user_it);
        return true;
    }

done:
    ecs_iter_fini(&user_it);
    return false;
}

static
ecs_entity_t flecs_observer_get_actual_event(
    ecs_observer_t *observer, 
    ecs_entity_t event)
{
    /* If operator is Not, reverse the event */
    if (observer->filter.terms[0].oper == EcsNot) {
        if (event == EcsOnAdd) {
            event = EcsOnRemove;
        } else if (event == EcsOnRemove) {
            event = EcsOnAdd;
        }
    }

    return event;
}

static
void flecs_unregister_event_observer(
    ecs_event_record_t *evt,
    ecs_id_t id)
{
    if (ecs_map_remove(&evt->event_ids, id) == 0) {
        ecs_map_fini(&evt->event_ids);
    }
}

static
ecs_event_id_record_t* flecs_ensure_event_id_record(
    ecs_map_t *map,
    ecs_id_t id)
{
    ecs_event_id_record_t **idt = ecs_map_ensure(
        map, ecs_event_id_record_t*, id);
    if (!idt[0]) {
        idt[0] = ecs_os_calloc_t(ecs_event_id_record_t);
    }

    return idt[0];
}

static
ecs_flags32_t flecs_id_flag_for_event(
    ecs_entity_t e)
{
    if (e == EcsOnAdd) {
        return EcsIdHasOnAdd;
    }
    if (e == EcsOnRemove) {
        return EcsIdHasOnRemove;
    }
    if (e == EcsOnSet) {
        return EcsIdHasOnSet;
    }
    if (e == EcsUnSet) {
        return EcsIdHasUnSet;
    }
    return 0;
}

static
void flecs_inc_observer_count(
    ecs_world_t *world,
    ecs_entity_t event,
    ecs_event_record_t *evt,
    ecs_id_t id,
    int32_t value)
{
    ecs_event_id_record_t *idt = flecs_ensure_event_id_record(&evt->event_ids, id);
    ecs_assert(idt != NULL, ECS_INTERNAL_ERROR, NULL);
    
    int32_t result = idt->observer_count += value;
    if (result == 1) {
        /* Notify framework that there are observers for the event/id. This 
         * allows parts of the code to skip event evaluation early */
        flecs_notify_tables(world, id, &(ecs_table_event_t){
            .kind = EcsTableTriggersForId,
            .event = event
        });

        ecs_flags32_t flags = flecs_id_flag_for_event(event);
        if (flags) {
            ecs_id_record_t *idr = flecs_id_record_get(world, id);
            if (idr) {
                idr->flags |= flags;
            }
        }
    } else if (result == 0) {
        /* Ditto, but the reverse */
        flecs_notify_tables(world, id, &(ecs_table_event_t){
            .kind = EcsTableNoTriggersForId,
            .event = event
        });

        ecs_flags32_t flags = flecs_id_flag_for_event(event);
        if (flags) {
            ecs_id_record_t *idr = flecs_id_record_get(world, id);
            if (idr) {
                idr->flags &= ~flags;
            }
        }

        /* Remove admin for id for event */
        if (!ecs_map_is_initialized(&idt->observers) && 
            !ecs_map_is_initialized(&idt->set_observers)) 
        {
            flecs_unregister_event_observer(evt, id);
            ecs_os_free(idt);
        }
    }
}

static
void flecs_register_observer_for_id(
    ecs_world_t *world,
    ecs_observable_t *observable,
    ecs_observer_t *observer,
    ecs_id_t id,
    size_t offset)
{
    ecs_sparse_t *events = observable->events;
    ecs_assert(events != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_id_t term_id = observer->register_id;

    int i;
    for (i = 0; i < observer->event_count; i ++) {
        ecs_entity_t event = flecs_observer_get_actual_event(
            observer, observer->events[i]);

        /* Get observers for event */
        ecs_event_record_t *evt = flecs_sparse_ensure(
            events, ecs_event_record_t, event);
        ecs_assert(evt != NULL, ECS_INTERNAL_ERROR, NULL);

        if (!ecs_map_is_initialized(&evt->event_ids)) {
            ecs_map_init(&evt->event_ids, ecs_event_id_record_t*, 1);
        }

        /* Get observers for (component) id for event */
        ecs_event_id_record_t *idt = flecs_ensure_event_id_record(
            &evt->event_ids, id);
        ecs_assert(idt != NULL, ECS_INTERNAL_ERROR, NULL);

        ecs_map_t *observers = ECS_OFFSET(idt, offset);
        if (!ecs_map_is_initialized(observers)) {
            ecs_map_init(observers, ecs_observer_t*, 1);
        }

        ecs_map_ensure(observers, ecs_observer_t*, 
            observer->entity)[0] = observer;

        flecs_inc_observer_count(world, event, evt, term_id, 1);
        if (term_id != id) {
            flecs_inc_observer_count(world, event, evt, id, 1);
        }
    }
}

static
void flecs_uni_observer_register(
    ecs_world_t *world,
    ecs_observable_t *observable,
    ecs_observer_t *observer)
{
    ecs_term_t *term = &observer->filter.terms[0];
    ecs_id_t id = observer->register_id;

    if (term->src.flags & EcsSelf) {
        if (ecs_term_match_this(term)) {
            flecs_register_observer_for_id(world, observable, observer, id,
                offsetof(ecs_event_id_record_t, observers));
        } else {
            flecs_register_observer_for_id(world, observable, observer, id,
                offsetof(ecs_event_id_record_t, entity_observers));
        }
    }

    if (observer->filter.terms[0].src.flags & EcsUp) {
        ecs_id_t pair = ecs_pair(term->src.trav, EcsWildcard);
        flecs_register_observer_for_id(world, observable, observer, pair,
            offsetof(ecs_event_id_record_t, set_observers));
    }
}

static
void flecs_unregister_observer_for_id(
    ecs_world_t *world,
    ecs_observable_t *observable,
    ecs_observer_t *observer,
    ecs_id_t id,
    size_t offset)
{
    ecs_sparse_t *events = observable->events;
    ecs_assert(events != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_id_t term_id = observer->register_id;

    int i;
    for (i = 0; i < observer->event_count; i ++) {
        ecs_entity_t event = flecs_observer_get_actual_event(
            observer, observer->events[i]);

        /* Get observers for event */
        ecs_event_record_t *evt = flecs_sparse_get(
            events, ecs_event_record_t, event);
        ecs_assert(evt != NULL, ECS_INTERNAL_ERROR, NULL);

        /* Get observers for (component) id */
        ecs_event_id_record_t *idt = ecs_map_get_ptr(
            &evt->event_ids, ecs_event_id_record_t*, id);
        ecs_assert(idt != NULL, ECS_INTERNAL_ERROR, NULL);

        ecs_map_t *id_observers = ECS_OFFSET(idt, offset);

        if (ecs_map_remove(id_observers, observer->entity) == 0) {
            ecs_map_fini(id_observers);
        }

        flecs_inc_observer_count(world, event, evt, term_id, -1);

        if (id != term_id) {
            /* Id is different from term_id in case of a set observer. If they're
             * the same, flecs_inc_observer_count could already have done cleanup */
            if (!ecs_map_is_initialized(&idt->observers) && 
                !ecs_map_is_initialized(&idt->set_observers) && 
                !idt->observer_count) 
            {
                flecs_unregister_event_observer(evt, id);
            }

            flecs_inc_observer_count(world, event, evt, id, -1);
        }
    }
}

static
void flecs_unregister_observer(
    ecs_world_t *world,
    ecs_observable_t *observable,
    ecs_observer_t *observer)
{
    ecs_assert(observer != NULL, ECS_INTERNAL_ERROR, NULL);
    if (!observer->filter.terms) {
        ecs_assert(observer->filter.term_count == 0, ECS_INTERNAL_ERROR, NULL);
        return;
    }

    ecs_term_t *term = &observer->filter.terms[0];
    ecs_id_t id = observer->register_id;

    if (term->src.flags & EcsSelf) {
        if (ecs_term_match_this(term)) {
            flecs_unregister_observer_for_id(world, observable, observer, id,
                offsetof(ecs_event_id_record_t, observers));
        } else {
            flecs_unregister_observer_for_id(world, observable, observer, id,
                offsetof(ecs_event_id_record_t, entity_observers));
        }
    }

    if (term->src.flags & EcsUp) {
        ecs_id_t pair = ecs_pair(term->src.trav, EcsWildcard);
        flecs_unregister_observer_for_id(world, observable, observer, pair,
            offsetof(ecs_event_id_record_t, set_observers));
    }
}

static
ecs_map_t* flecs_get_observers_for_event(
    const ecs_observable_t *observable,
    ecs_entity_t event)
{
    ecs_check(observable != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(event != 0, ECS_INTERNAL_ERROR, NULL);

    ecs_sparse_t *events = observable->events;
    ecs_assert(events != NULL, ECS_INTERNAL_ERROR, NULL);

    const ecs_event_record_t *evt = flecs_sparse_get(
        events, ecs_event_record_t, event);
    
    if (evt) {
        return (ecs_map_t*)&evt->event_ids;
    }

error:
    return NULL;
}

static
ecs_event_id_record_t* flecs_get_observers_for_id(
    const ecs_map_t *evt,
    ecs_id_t id)
{
    return ecs_map_get_ptr(evt, ecs_event_id_record_t*, id);
}

static
void flecs_init_observer_iter(
    ecs_iter_t *it,
    bool *iter_set)
{
    ecs_assert(it != NULL, ECS_INTERNAL_ERROR, NULL);
    
    if (*iter_set) {
        return;
    }

    if (ECS_BIT_IS_SET(it->flags, EcsIterTableOnly)) {
        it->ids = it->priv.cache.ids;
        it->ids[0] = it->event_id;
        return;
    }

    flecs_iter_init(it, flecs_iter_cache_all);
    flecs_iter_validate(it);

    *iter_set = true;

    it->ids[0] = it->event_id;

    ecs_assert(it->table != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(!it->count || it->offset < ecs_table_count(it->table), 
        ECS_INTERNAL_ERROR, NULL);
    ecs_assert((it->offset + it->count) <= ecs_table_count(it->table), 
        ECS_INTERNAL_ERROR, NULL);

    int32_t index = ecs_search_relation(it->real_world, it->table, 0, 
        it->event_id, EcsIsA, 0, it->sources, 0, 0);
    
    if (index == -1) {
        it->columns[0] = 0;
    } else if (it->sources[0]) {
        it->columns[0] = -index - 1;
    } else {
        it->columns[0] = index + 1;
    }

    ecs_term_t term = {
        .id = it->event_id
    };

    it->term_count = 1;
    it->terms = &term;
    flecs_iter_populate_data(it->real_world, it, it->table, it->offset, 
        it->count, it->ptrs, it->sizes);
}

static
bool flecs_ignore_observer(
    ecs_world_t *world,
    ecs_observer_t *observer,
    ecs_table_t *table)
{
    int32_t *last_event_id = observer->last_event_id;
    if (last_event_id && last_event_id[0] == world->event_id) {
        return true;
    }

    if (!table) {
        return false;
    }
    
    ecs_filter_t *filter = &observer->filter;
    if (!(filter->flags & EcsFilterMatchPrefab) && 
        (table->flags & EcsTableIsPrefab)) 
    {
        return true;
    }
    if (!(filter->flags & EcsFilterMatchDisabled) && 
        (table->flags & EcsTableIsDisabled)) 
    {
        return true;
    }
    
    return false;
}

bool ecs_observer_default_run_action(ecs_iter_t *it) {
    ecs_observer_t *observer = it->ctx;
    if (observer->is_multi) {
        return flecs_multi_observer_invoke(it);
    } else {
        it->callback(it);
        return true;
    }
}

static 
void flecs_default_multi_observer_run_callback(ecs_iter_t *it) {
    flecs_multi_observer_invoke(it);
}

static 
void flecs_default_uni_observer_run_callback(ecs_iter_t *it) {
    ecs_observer_t *observer = it->ctx;
    it->ctx = observer->ctx;
    it->callback = observer->callback;
    it->callback(it);
}

/* For convenience, so applications can (in theory) use a single run callback 
 * that uses ecs_iter_next to iterate results */
static 
bool flecs_default_observer_next_callback(ecs_iter_t *it) {
    if (it->interrupted_by) {
        return false;
    } else {
        /* Use interrupted_by to signal the next iteration must return false */
        it->interrupted_by = it->system;
        return true;
    }
}

/* Run action for children of multi observer */
static
void flecs_multi_observer_builtin_run(ecs_iter_t *it) {
    ecs_observer_t *observer = it->ctx;
    ecs_run_action_t run = observer->run;

    if (run) {
        it->next = flecs_default_observer_next_callback;
        it->callback = flecs_default_multi_observer_run_callback;
        it->interrupted_by = 0;
        run(it);
    } else {
        flecs_multi_observer_invoke(it);
    }
}

/* Run action for uni observer */
static
void flecs_uni_observer_builtin_run(
    ecs_observer_t *observer,
    ecs_iter_t *it)
{
    ECS_BIT_COND(it->flags, EcsIterIsFilter, 
        observer->filter.terms[0].inout == EcsInOutNone);

    it->system = observer->entity;
    it->ctx = observer->ctx;
    it->binding_ctx = observer->binding_ctx;
    it->term_index = observer->term_index;
    it->terms = observer->filter.terms;

    void *ptrs = it->ptrs;
    if (it->flags & EcsIterIsFilter) {
        it->ptrs = NULL;
    }

    ecs_entity_t event = it->event;
    it->event = flecs_observer_get_actual_event(observer, event);

    if (observer->run) {
        it->next = flecs_default_observer_next_callback;
        it->callback = flecs_default_uni_observer_run_callback;
        it->ctx = observer;
        observer->run(it);
    } else {
        observer->callback(it);
    }

    it->event = event;
    it->ptrs = ptrs;
}

static
void flecs_notify_self_observers(
    ecs_world_t *world,
    ecs_iter_t *it,
    const ecs_map_t *observers)
{
    ecs_assert(observers != NULL, ECS_INTERNAL_ERROR, NULL);

    ecs_map_iter_t mit = ecs_map_iter(observers);
    ecs_observer_t *observer;
    while ((observer = ecs_map_next_ptr(&mit, ecs_observer_t*, NULL))) {
        if (flecs_ignore_observer(world, observer, it->table)) {
            continue;
        }
        flecs_uni_observer_builtin_run(observer, it);
    }
}

static
void flecs_notify_entity_observers(
    ecs_world_t *world,
    ecs_iter_t *it,
    const ecs_map_t *observers)
{
    ecs_assert(observers != NULL, ECS_INTERNAL_ERROR, NULL);

    if (ECS_BIT_IS_SET(it->flags, EcsIterTableOnly)) {
        return;
    }

    ecs_map_iter_t mit = ecs_map_iter(observers);
    ecs_observer_t *observer;
    int32_t offset = it->offset, count = it->count;
    ecs_entity_t *entities = it->entities;
    
    ecs_entity_t dummy = 0;
    it->entities = &dummy;

    while ((observer = ecs_map_next_ptr(&mit, ecs_observer_t*, NULL))) {
        if (flecs_ignore_observer(world, observer, it->table)) {
            continue;
        }

        int32_t i, entity_count = it->count;
        for (i = 0; i < entity_count; i ++) {
            if (entities[i] != observer->filter.terms[0].src.id) {
                continue;
            }

            it->offset = i;
            it->count = 1;
            it->sources[0] = entities[i];
            flecs_uni_observer_builtin_run(observer, it);
        }
    }

    it->offset = offset;
    it->count = count;
    it->entities = entities;
    it->sources[0] = 0;
}

static
void flecs_notify_set_base_observers(
    ecs_world_t *world,
    ecs_iter_t *it,
    const ecs_map_t *observers)
{
    ecs_assert(observers != NULL, ECS_INTERNAL_ERROR, NULL);

    ecs_entity_t event_id = it->event_id;
    ecs_entity_t rel = ECS_PAIR_FIRST(event_id);
    ecs_entity_t obj = ecs_pair_second(world, event_id);
    if (!obj) {
        /* Don't notify for deleted (or in progress of being deleted) object */
        return;
    }

    ecs_record_t *obj_record = flecs_entities_get(world, obj);
    if (!obj_record) {
        return;
    }

    ecs_table_t *obj_table = obj_record->table;
    if (!obj_table) {
        return;
    }

    ecs_map_iter_t mit = ecs_map_iter(observers);
    ecs_observer_t *observer;
    while ((observer = ecs_map_next_ptr(&mit, ecs_observer_t*, NULL))) {
        if (flecs_ignore_observer(world, observer, it->table)) {
            continue;
        }

        ecs_term_t *term = &observer->filter.terms[0];
        ecs_id_t id = term->id;
        int32_t column = ecs_search_relation(world, obj_table, 0, id, rel, 
            0, it->sources, it->ids, 0);

        bool result = column != -1;
        if (term->oper == EcsNot) {
            result = !result;
        }
        if (!result) {
            continue;
        }

        if ((term->src.flags & EcsSelf) && flecs_table_record_get(
            world, it->table, id) != NULL) 
        {
            continue;
        }

        if (!ECS_BIT_IS_SET(it->flags, EcsIterTableOnly)) {
            if (!it->sources[0]) {
                it->sources[0] = obj;
            }

            /* Populate pointer from object */
            int32_t s_column = ecs_table_type_to_storage_index(
                obj_table, column);
            if (s_column != -1) {
                ecs_column_t *c = &obj_table->data.columns[s_column];
                int32_t row = ECS_RECORD_TO_ROW(obj_record->row);
                ecs_type_info_t *ti = obj_table->type_info[s_column];
                void *ptr = ecs_storage_get(c, ti->size, row);
                it->ptrs[0] = ptr;
                it->sizes[0] = ti->size;
            }
        }

        it->event_id = observer->filter.terms[0].id;
        flecs_uni_observer_builtin_run(observer, it);
    }
}

static
void flecs_notify_set_observers(
    ecs_world_t *world,
    ecs_iter_t *it,
    const ecs_map_t *observers)
{
    ecs_assert(observers != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(it->count != 0, ECS_INTERNAL_ERROR, NULL);

    if (ECS_BIT_IS_SET(it->flags, EcsIterTableOnly)) {
        return;
    }

    ecs_map_iter_t mit = ecs_map_iter(observers);
    ecs_observer_t *observer;
    while ((observer = ecs_map_next_ptr(&mit, ecs_observer_t*, NULL))) {
        if (!ecs_id_match(it->event_id, observer->filter.terms[0].id)) {
            continue;
        }

        if (flecs_ignore_observer(world, observer, it->table)) {
            continue;
        }

        ecs_entity_t src = it->entities[0];
        int32_t i, count = it->count;
        ecs_filter_t *filter = &observer->filter;
        ecs_term_t *term = &filter->terms[0];
        ecs_entity_t src_id = term->src.id;

        /* If observer is for a specific entity, make sure it is in the table
         * being triggered for */
        if (!(ecs_term_match_this(term))) {
            for (i = 0; i < count; i ++) {
                if (it->entities[i] == src_id) {
                    break;
                }
            }

            if (i == count) {
                continue;
            }

            /* If the entity matches, observer for no other entities */
            it->entities[0] = 0;
            it->count = 1;
        }

        if (flecs_term_match_table(world, term, it->table, it->ids, 
            it->columns, it->sources, NULL, true, it->flags))
        {
            if (!it->sources[0]) {
                /* Do not match owned components */
                continue;
            }

            /* Triggers for supersets can be instanced */
            bool instanced = filter->flags & EcsFilterIsInstanced;
            bool is_filter = ECS_BIT_IS_SET(it->flags, EcsIterIsFilter);
            if (it->count == 1 || instanced || is_filter || !it->sizes[0]) {
                ECS_BIT_COND(it->flags, EcsIterIsInstanced, instanced);
                flecs_uni_observer_builtin_run(observer, it);
                ECS_BIT_CLEAR(it->flags, EcsIterIsInstanced);
            } else {
                ecs_entity_t *entities = it->entities;
                it->count = 1;
                for (i = 0; i < count; i ++) {
                    it->entities = &entities[i];
                    flecs_uni_observer_builtin_run(observer, it);
                }
                it->entities = entities;
            }
        }

        it->entities[0] = src;
        it->count = count;            
    }
}

static
void flecs_notify_observers_for_id(
    ecs_world_t *world,
    const ecs_map_t *evt,
    ecs_id_t event_id,
    ecs_iter_t *it,
    bool *iter_set)
{
    const ecs_event_id_record_t *idt = flecs_get_observers_for_id(evt, event_id);
    if (!idt) {
        return;
    }

    if (ecs_map_is_initialized(&idt->observers)) {
        flecs_init_observer_iter(it, iter_set);
        flecs_notify_self_observers(world, it, &idt->observers);
    }
    if (ecs_map_is_initialized(&idt->entity_observers)) {
        flecs_init_observer_iter(it, iter_set);
        flecs_notify_entity_observers(world, it, &idt->entity_observers);
    }
    if (ecs_map_is_initialized(&idt->set_observers)) {
        flecs_init_observer_iter(it, iter_set);
        flecs_notify_set_base_observers(world, it, &idt->set_observers);
    }
}

static
void flecs_notify_set_observers_for_id(
    ecs_world_t *world,
    const ecs_map_t *evt,
    ecs_iter_t *it,
    bool *iter_set,
    ecs_id_t set_id)
{
    const ecs_event_id_record_t *idt = flecs_get_observers_for_id(evt, set_id);
    if (idt && ecs_map_is_initialized(&idt->set_observers)) {
        flecs_init_observer_iter(it, iter_set);
        flecs_notify_set_observers(world, it, &idt->set_observers);
    }
}

static
void flecs_uni_observer_trigger_existing(
    ecs_world_t *world,
    ecs_observer_t *observer)
{
    ecs_iter_action_t callback = observer->callback;

    /* If yield existing is enabled, observer for each thing that matches
     * the event, if the event is iterable. */
    int i, count = observer->event_count;
    for (i = 0; i < count; i ++) {
        ecs_entity_t evt = observer->events[i];
        const EcsIterable *iterable = ecs_get(world, evt, EcsIterable);
        if (!iterable) {
            continue;
        }

        ecs_iter_t it;
        iterable->init(world, world, &it, &observer->filter.terms[0]);
        it.system = observer->entity;
        it.ctx = observer->ctx;
        it.binding_ctx = observer->binding_ctx;
        it.event = evt;

        ecs_iter_next_action_t next = it.next;
        ecs_assert(next != NULL, ECS_INTERNAL_ERROR, NULL);
        while (next(&it)) {
            it.event_id = it.ids[0];
            callback(&it);
        }
    }
}

static
void flecs_multi_observer_yield_existing(
    ecs_world_t *world,
    ecs_observer_t *observer)
{
    ecs_run_action_t run = observer->run;
    if (!run) {
        run = flecs_default_multi_observer_run_callback;
    }

    int32_t pivot_term = ecs_filter_pivot_term(world, &observer->filter);
    if (pivot_term < 0) {
        return;
    }

    /* If yield existing is enabled, invoke for each thing that matches
     * the event, if the event is iterable. */
    int i, count = observer->event_count;
    for (i = 0; i < count; i ++) {
        ecs_entity_t evt = observer->events[i];
        const EcsIterable *iterable = ecs_get(world, evt, EcsIterable);
        if (!iterable) {
            continue;
        }

        ecs_iter_t it;
        iterable->init(world, world, &it, &observer->filter.terms[pivot_term]);
        it.terms = observer->filter.terms;
        it.term_count = 1;
        it.term_index = pivot_term;
        it.system = observer->entity;
        it.ctx = observer;
        it.binding_ctx = observer->binding_ctx;
        it.event = evt;

        ecs_iter_next_action_t next = it.next;
        ecs_assert(next != NULL, ECS_INTERNAL_ERROR, NULL);
        while (next(&it)) {
            run(&it);
            world->event_id ++;
        }
    }
}

bool flecs_check_observers_for_event(
    const ecs_poly_t *object,
    ecs_id_t id,
    ecs_entity_t event)
{    
    ecs_observable_t *observable = ecs_get_observable(object);
    const ecs_map_t *evt = flecs_get_observers_for_event(observable, event);
    if (!evt) {
        return false;
    }

    ecs_event_id_record_t *edr = flecs_get_observers_for_id(evt, id);
    if (edr) {
        return edr->observer_count != 0;
    } else {
        return false;
    }
}

void flecs_observers_notify(
    ecs_iter_t *it,
    ecs_observable_t *observable,
    const ecs_type_t *ids,
    ecs_entity_t event)
{
    ecs_assert(ids != NULL && ids->count != 0, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(ids->array != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_entity_t events[2] = {event, EcsWildcard};
    int32_t e, i, ids_count = ids->count;
    ecs_id_t *ids_array = ids->array;
    ecs_world_t *world = it->real_world;

    for (e = 0; e < 2; e ++) {
        event = events[e];
        const ecs_map_t *evt = flecs_get_observers_for_event(observable, event);
        if (!evt) {
            continue;
        }

        it->event = event;

        for (i = 0; i < ids_count; i ++) {
            ecs_id_t id = ids_array[i];
            ecs_entity_t role = id & ECS_ROLE_MASK;
            bool iter_set = false;

            it->event_id = id;

            flecs_notify_observers_for_id(world, evt, id, it, &iter_set);

            if (role == ECS_PAIR) {
                ecs_entity_t r = ECS_PAIR_FIRST(id);
                ecs_entity_t o = ECS_PAIR_SECOND(id);

                ecs_id_t tid = ecs_pair(r, EcsWildcard);
                flecs_notify_observers_for_id(world, evt, tid, it, &iter_set);

                tid = ecs_pair(EcsWildcard, o);
                flecs_notify_observers_for_id(world, evt, tid, it, &iter_set);
                
                tid = ecs_pair(EcsWildcard, EcsWildcard);
                flecs_notify_observers_for_id(world, evt, tid, it, &iter_set);
            } else {
                flecs_notify_observers_for_id(
                    world, evt, EcsWildcard, it, &iter_set);
            }

            flecs_notify_observers_for_id(world, evt, EcsAny, it, &iter_set);

            if (iter_set) {
                ecs_iter_fini(it);
            }
        }
    }
}

void flecs_set_observers_notify(
    ecs_iter_t *it,
    ecs_observable_t *observable,
    const ecs_type_t *ids,
    ecs_entity_t event,
    ecs_id_t set_id)
{
    ecs_assert(ids != NULL && ids->count != 0, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(ids->array != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_entity_t events[2] = {event, EcsWildcard};
    int32_t e, i, ids_count = ids->count;
    ecs_id_t *ids_array = ids->array;
    ecs_world_t *world = it->real_world;

    for (e = 0; e < 2; e ++) {
        event = events[e];
        const ecs_map_t *evt = flecs_get_observers_for_event(observable, event);
        if (!evt) {
            continue;
        }

        it->event = event;

        for (i = 0; i < ids_count; i ++) {
            ecs_id_t id = ids_array[i];
            bool iter_set = false;

            it->event_id = id;

            flecs_notify_set_observers_for_id(world, evt, it, &iter_set, set_id);

            if (iter_set) {
                ecs_iter_fini(it);
            }
        }
    }
}

static
int flecs_uni_observer_init(
    ecs_world_t *world,
    ecs_observer_t *observer,
    const ecs_observer_desc_t *desc)
{
    ecs_term_t *term = &observer->filter.terms[0];
    observer->last_event_id = desc->last_event_id;    
    observer->register_id = flecs_from_public_id(world, term->id);
    term->index = desc->term_index;

    if (ecs_id_is_tag(world, term->id)) {
        /* If id is a tag, downgrade OnSet/UnSet to OnAdd/OnRemove. */
        int32_t e, count = observer->event_count;
        for (e = 0; e < count; e ++) {
            if (observer->events[e] == EcsOnSet) {
                observer->events[e] = EcsOnAdd;
            } else
            if (observer->events[e] == EcsUnSet) {
                observer->events[e] = EcsOnRemove;
            }
        }
    }

    flecs_uni_observer_register(world, observer->observable, observer);

    if (desc->yield_existing) {
        flecs_uni_observer_trigger_existing(world, observer);
    }

    return 0;
}

static
int flecs_multi_observer_init(
    ecs_world_t *world,
    ecs_observer_t *observer,
    const ecs_observer_desc_t *desc)
{
    /* Create last event id for filtering out the same event that arrives from
     * more than one term */
    observer->last_event_id = ecs_os_calloc_t(int32_t);
    
    /* Mark observer as multi observer */
    observer->is_multi = true;

    /* Create a child observer for each term in the filter */
    ecs_filter_t *filter = &observer->filter;
    ecs_observer_desc_t child_desc = *desc;
    child_desc.last_event_id = observer->last_event_id;
    child_desc.run = NULL;
    child_desc.callback = flecs_multi_observer_builtin_run;
    child_desc.ctx = observer;
    child_desc.filter.expr = NULL;
    child_desc.filter.terms_buffer = NULL;
    child_desc.filter.terms_buffer_count = 0;
    child_desc.binding_ctx = NULL;
    child_desc.binding_ctx_free = NULL;
    ecs_os_zeromem(&child_desc.entity);
    ecs_os_zeromem(&child_desc.filter.terms);
    ecs_os_memcpy_n(child_desc.events, observer->events, 
        ecs_entity_t, observer->event_count);

    int i, term_count = filter->term_count;
    bool optional_only = filter->flags & EcsFilterMatchThis;
    for (i = 0; i < term_count; i ++) {
        if (filter->terms[i].oper != EcsOptional) {
            if (ecs_term_match_this(&filter->terms[i])) {
                optional_only = false;
            }
        }
    }

    if (filter->flags & EcsFilterMatchPrefab) {
        child_desc.filter.flags |= EcsFilterMatchPrefab;
    }
    if (filter->flags & EcsFilterMatchDisabled) {
        child_desc.filter.flags |= EcsFilterMatchDisabled;
    }

    /* Create observers as children of observer */
    ecs_entity_t old_scope = ecs_set_scope(world, observer->entity);

    for (i = 0; i < term_count; i ++) {
        ecs_term_t *term = &child_desc.filter.terms[0];
        child_desc.term_index = filter->terms[i].index;
        *term = filter->terms[i];

        ecs_oper_kind_t oper = term->oper;
        ecs_id_t id = term->id;

        /* AndFrom & OrFrom terms insert multiple observers */
        if (oper == EcsAndFrom || oper == EcsOrFrom) {
            const ecs_type_t *type = ecs_get_type(world, id);
            int32_t ti, ti_count = type->count;
            ecs_id_t *ti_ids = type->array;

            /* Correct operator will be applied when an event occurs, and
             * the observer is evaluated on the observer source */
            term->oper = EcsAnd;
            for (ti = 0; ti < ti_count; ti ++) {
                ecs_id_t ti_id = ti_ids[ti];
                ecs_id_record_t *idr = flecs_id_record_get(world, ti_id);
                if (idr->flags & EcsIdDontInherit) {
                    continue;
                }

                term->first.name = NULL;
                term->first.id = ti_ids[ti];
                term->id = ti_ids[ti];

                if (ecs_observer_init(world, &child_desc) == 0) {
                    goto error;
                }
            }
            continue;
        }

        /* If observer only contains optional terms, match everything */
        if (optional_only) {
            term->id = EcsAny;
            term->first.id = EcsAny;
            term->src.id = EcsThis;
            term->src.flags = EcsIsVariable;
            term->second.id = 0;
        }
        if (ecs_observer_init(world, &child_desc) == 0) {
            goto error;
        }

        if (optional_only) {
            break;
        }
    }

    ecs_set_scope(world, old_scope);

    if (desc->yield_existing) {
        flecs_multi_observer_yield_existing(world, observer);
    }

    return 0; 
error:
    return -1;
}

ecs_entity_t ecs_observer_init(
    ecs_world_t *world,
    const ecs_observer_desc_t *desc)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(desc != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(desc->_canary == 0, ECS_INVALID_PARAMETER, NULL);
    ecs_check(!(world->flags & EcsWorldFini), ECS_INVALID_OPERATION, NULL);

    ecs_entity_t entity = desc->entity;
    if (!entity) {
        entity = ecs_new(world, 0);
    }
    EcsPoly *poly = ecs_poly_bind(world, entity, ecs_observer_t);
    if (!poly->poly) {
        ecs_check(desc->callback != NULL || desc->run != NULL, 
            ECS_INVALID_OPERATION, NULL);

        ecs_observer_t *observer = ecs_poly_new(ecs_observer_t);
        ecs_assert(observer != NULL, ECS_INTERNAL_ERROR, NULL);
        
        observer->world = world;
        observer->dtor = (ecs_poly_dtor_t)flecs_observer_fini;
        observer->entity = entity;

        /* Make writeable copy of filter desc so that we can set name. This will
         * make debugging easier, as any error messages related to creating the
         * filter will have the name of the observer. */
        ecs_filter_desc_t filter_desc = desc->filter;
        filter_desc.name = ecs_get_name(world, entity);
        ecs_filter_t *filter = filter_desc.storage = &observer->filter;
        *filter = ECS_FILTER_INIT;

        /* Parse filter */
        if (ecs_filter_init(world, &filter_desc) == NULL) {
            flecs_observer_fini(observer);
            return 0;
        }

        /* Observer must have at least one term */
        ecs_check(observer->filter.term_count > 0, ECS_INVALID_PARAMETER, NULL);

        poly->poly = observer;

        ecs_observable_t *observable = desc->observable;
        if (!observable) {
            observable = ecs_get_observable(world);
        }

        observer->run = desc->run;
        observer->callback = desc->callback;
        observer->ctx = desc->ctx;
        observer->binding_ctx = desc->binding_ctx;
        observer->ctx_free = desc->ctx_free;
        observer->binding_ctx_free = desc->binding_ctx_free;
        observer->term_index = desc->term_index;
        observer->observable = observable;

        /* Check if observer is monitor. Monitors are created as multi observers
         * since they require pre/post checking of the filter to test if the
         * entity is entering/leaving the monitor. */
        int i;
        for (i = 0; i < ECS_OBSERVER_DESC_EVENT_COUNT_MAX; i ++) {
            ecs_entity_t event = desc->events[i];
            if (!event) {
                break;
            }

            if (event == EcsMonitor) {
                /* Monitor event must be first and last event */
                ecs_check(i == 0, ECS_INVALID_PARAMETER, NULL);

                observer->events[0] = EcsOnAdd;
                observer->events[1] = EcsOnRemove;
                observer->event_count ++;
                observer->is_monitor = true;
            } else {
                observer->events[i] = event;
            }

            observer->event_count ++;
        }

        /* Observer must have at least one event */
        ecs_check(observer->event_count != 0, ECS_INVALID_PARAMETER, NULL);

        bool multi = false;

        if (filter->term_count == 1 && !desc->last_event_id) {
            ecs_term_t *term = &filter->terms[0];
            /* If the filter has a single term but it is a *From operator, we
             * need to create a multi observer */
            multi |= (term->oper == EcsAndFrom) || (term->oper == EcsOrFrom);
            
            /* An observer with only optional terms is a special case that is
             * only handled by multi observers */
            multi |= term->oper == EcsOptional;
        }

        if (filter->term_count == 1 && !observer->is_monitor && !multi) {
            if (flecs_uni_observer_init(world, observer, desc)) {
                goto error;
            }
        } else {
            if (flecs_multi_observer_init(world, observer, desc)) {
                goto error;
            }
        }

        if (ecs_get_name(world, entity)) {
            ecs_trace("#[green]observer#[reset] %s created", 
                ecs_get_name(world, entity));
        }
    } else {
        /* If existing entity handle was provided, override existing params */
        if (desc->callback) {
            ecs_poly(poly->poly, ecs_observer_t)->callback = desc->callback;
        }
        if (desc->ctx) {
            ecs_poly(poly->poly, ecs_observer_t)->ctx = desc->ctx;
        }
        if (desc->binding_ctx) {
            ecs_poly(poly->poly, ecs_observer_t)->binding_ctx = desc->binding_ctx;
        }
    }

    ecs_poly_modified(world, entity, ecs_observer_t);

    return entity;
error:
    ecs_delete(world, entity);
    return 0;
}

void* ecs_get_observer_ctx(
    const ecs_world_t *world,
    ecs_entity_t observer)
{
    const EcsPoly *o = ecs_poly_bind_get(world, observer, ecs_observer_t);
    if (o) {
        ecs_poly_assert(o->poly, ecs_observer_t);
        return ((ecs_observer_t*)o->poly)->ctx;
    } else {
        return NULL;
    }     
}

void* ecs_get_observer_binding_ctx(
    const ecs_world_t *world,
    ecs_entity_t observer)
{
    const EcsPoly *o = ecs_poly_bind_get(world, observer, ecs_observer_t);
    if (o) {
        ecs_poly_assert(o->poly, ecs_observer_t);
        return ((ecs_observer_t*)o->poly)->binding_ctx;
    } else {
        return NULL;
    }      
}

void flecs_observer_fini(
    ecs_observer_t *observer)
{
    if (observer->is_multi) {
        /* Child observers get deleted up by entity cleanup logic */
        ecs_os_free(observer->last_event_id);
    } else {
        if (observer->filter.term_count) {
            flecs_unregister_observer(
                observer->world, observer->observable, observer);
        } else {
            /* Observer creation failed while creating filter */
        }
    }

    /* Cleanup filters */
    ecs_filter_fini(&observer->filter);

    /* Cleanup context */
    if (observer->ctx_free) {
        observer->ctx_free(observer->ctx);
    }

    if (observer->binding_ctx_free) {
        observer->binding_ctx_free(observer->binding_ctx);
    }

    ecs_poly_free(observer, ecs_observer_t);
}


static
void table_cache_list_remove(
    ecs_table_cache_t *cache,
    ecs_table_cache_hdr_t *elem)
{
    ecs_table_cache_hdr_t *next = elem->next;
    ecs_table_cache_hdr_t *prev = elem->prev;

    if (next) {
        next->prev = prev;
    }
    if (prev) {
        prev->next = next;
    }

    cache->empty_tables.count -= !!elem->empty;
    cache->tables.count -= !elem->empty;

    if (cache->empty_tables.first == elem) {
        cache->empty_tables.first = next;
    } else if (cache->tables.first == elem) {
        cache->tables.first = next;
    }
    if (cache->empty_tables.last == elem) {
        cache->empty_tables.last = prev;
    }
    if (cache->tables.last == elem) {
        cache->tables.last = prev;
    }
}

static
void table_cache_list_insert(
    ecs_table_cache_t *cache,
    ecs_table_cache_hdr_t *elem)
{
    ecs_table_cache_hdr_t *last;
    if (elem->empty) {
        last = cache->empty_tables.last;
        cache->empty_tables.last = elem;
        if ((++ cache->empty_tables.count) == 1) {
            cache->empty_tables.first = elem;
        }
    } else {
        last = cache->tables.last;
        cache->tables.last = elem;
        if ((++ cache->tables.count) == 1) {
            cache->tables.first = elem;
        }
    }

    elem->next = NULL;
    elem->prev = last;

    if (last) {
        last->next = elem;
    }
}

void ecs_table_cache_init(
    ecs_table_cache_t *cache)
{
    ecs_assert(cache != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_map_init(&cache->index, ecs_table_cache_hdr_t*, 4);
}

void ecs_table_cache_fini(
    ecs_table_cache_t *cache)
{
    ecs_assert(cache != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_map_fini(&cache->index);
}

bool ecs_table_cache_is_empty(
    const ecs_table_cache_t *cache)
{
    return ecs_map_count(&cache->index) == 0;
}

void ecs_table_cache_insert(
    ecs_table_cache_t *cache,
    const ecs_table_t *table,
    ecs_table_cache_hdr_t *result)
{
    ecs_assert(cache != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(ecs_table_cache_get(cache, table) == NULL,
        ECS_INTERNAL_ERROR, NULL);
    ecs_assert(result != NULL, ECS_INTERNAL_ERROR, NULL);

    bool empty;
    if (!table) {
        empty = false;
    } else {
        empty = ecs_table_count(table) == 0;
    }

    result->cache = cache;
    result->table = (ecs_table_t*)table;
    result->empty = empty;

    table_cache_list_insert(cache, result);

    if (table) {
        ecs_map_set_ptr(&cache->index, table->id, result);
    }

    ecs_assert(empty || cache->tables.first != NULL, 
        ECS_INTERNAL_ERROR, NULL);
    ecs_assert(!empty || cache->empty_tables.first != NULL, 
        ECS_INTERNAL_ERROR, NULL);
}

void ecs_table_cache_replace(
    ecs_table_cache_t *cache,
    const ecs_table_t *table,
    ecs_table_cache_hdr_t *elem)
{
    ecs_table_cache_hdr_t **oldptr = ecs_map_get(&cache->index, 
        ecs_table_cache_hdr_t*, table->id);
    ecs_assert(oldptr != NULL, ECS_INTERNAL_ERROR, NULL);

    ecs_table_cache_hdr_t *old = *oldptr;
    ecs_assert(old != NULL, ECS_INTERNAL_ERROR, NULL);

    ecs_table_cache_hdr_t *prev = old->prev, *next = old->next;
    if (prev) {
        ecs_assert(prev->next == old, ECS_INTERNAL_ERROR, NULL);
        prev->next = elem;
    }
    if (next) {
        ecs_assert(next->prev == old, ECS_INTERNAL_ERROR, NULL);
        next->prev = elem;
    }

    if (cache->empty_tables.first == old) {
        cache->empty_tables.first = elem;
    }
    if (cache->empty_tables.last == old) {
        cache->empty_tables.last = elem;
    }
    if (cache->tables.first == old) {
        cache->tables.first = elem;
    }
    if (cache->tables.last == old) {
        cache->tables.last = elem;
    }

    *oldptr = elem;
    elem->prev = prev;
    elem->next = next;
}

void* ecs_table_cache_get(
    const ecs_table_cache_t *cache,
    const ecs_table_t *table)
{
    ecs_assert(cache != NULL, ECS_INTERNAL_ERROR, NULL);
    if (table) {
        return ecs_map_get_ptr(&cache->index, ecs_table_cache_hdr_t*, table->id);
    } else {
        ecs_table_cache_hdr_t *elem = cache->tables.first;
        ecs_assert(!elem || elem->table == NULL, ECS_INTERNAL_ERROR, NULL);
        return elem;
    }
}

void* ecs_table_cache_remove(
    ecs_table_cache_t *cache,
    const ecs_table_t *table,
    ecs_table_cache_hdr_t *elem)
{
    ecs_assert(cache != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(table != NULL, ECS_INTERNAL_ERROR, NULL);

    if (!ecs_map_is_initialized(&cache->index)) {
        return NULL;
    }

    if (!elem) {
        elem = ecs_map_get_ptr(
            &cache->index, ecs_table_cache_hdr_t*, table->id);
        if (!elem) {
            return false;
        }
    }

    ecs_assert(elem != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(elem->cache == cache, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(elem->table == table, ECS_INTERNAL_ERROR, NULL);

    table_cache_list_remove(cache, elem);

    ecs_map_remove(&cache->index, table->id);

    return elem;
}

bool ecs_table_cache_set_empty(
    ecs_table_cache_t *cache,
    const ecs_table_t *table,
    bool empty)
{
    ecs_assert(cache != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(table != NULL, ECS_INTERNAL_ERROR, NULL);

    ecs_table_cache_hdr_t *elem = ecs_map_get_ptr(
        &cache->index, ecs_table_cache_hdr_t*, table->id);
    if (!elem) {
        return false;
    }

    if (elem->empty == empty) {
        return false;
    }

    table_cache_list_remove(cache, elem);
    elem->empty = empty;
    table_cache_list_insert(cache, elem);

    return true;
}

bool flecs_table_cache_iter(
    ecs_table_cache_t *cache,
    ecs_table_cache_iter_t *out)
{
    ecs_assert(cache != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(out != NULL, ECS_INTERNAL_ERROR, NULL);
    out->next = cache->tables.first;
    out->next_list = NULL;
    out->cur = NULL;
    return out->next != NULL;
}

bool flecs_table_cache_empty_iter(
    ecs_table_cache_t *cache,
    ecs_table_cache_iter_t *out)
{
    ecs_assert(cache != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(out != NULL, ECS_INTERNAL_ERROR, NULL);
    out->next = cache->empty_tables.first;
    out->next_list = NULL;
    out->cur = NULL;
    return out->next != NULL;
}

bool flecs_table_cache_all_iter(
    ecs_table_cache_t *cache,
    ecs_table_cache_iter_t *out)
{
    ecs_assert(cache != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(out != NULL, ECS_INTERNAL_ERROR, NULL);
    out->next = cache->empty_tables.first;
    out->next_list = cache->tables.first;
    out->cur = NULL;
    return out->next != NULL || out->next_list != NULL;
}

ecs_table_cache_hdr_t* _flecs_table_cache_next(
    ecs_table_cache_iter_t *it)
{
    ecs_table_cache_hdr_t *next = it->next;
    if (!next) {
        next = it->next_list;
        it->next_list = NULL;
        if (!next) {
            return false;
        }
    }

    it->cur = next;
    it->next = next->next;
    return next;
}

#include <stdio.h>
#include <ctype.h>
#include <time.h>

void ecs_os_api_impl(ecs_os_api_t *api);

static bool ecs_os_api_initialized = false;
static bool ecs_os_api_initializing = false;
static int ecs_os_api_init_count = 0;

#ifndef __EMSCRIPTEN__
ecs_os_api_t ecs_os_api = {
    .flags_ = EcsOsApiHighResolutionTimer | EcsOsApiLogWithColors,
    .log_level_ = -1 /* Disable tracing by default, but log warnings/errors */
};
#else
/* Disable colors by default for emscripten */
ecs_os_api_t ecs_os_api = {
    .flags_ = EcsOsApiHighResolutionTimer,
    .log_level_ = -1 /* Disable tracing by default, but log warnings/errors */
};
#endif

int64_t ecs_os_api_malloc_count = 0;
int64_t ecs_os_api_realloc_count = 0;
int64_t ecs_os_api_calloc_count = 0;
int64_t ecs_os_api_free_count = 0;

void ecs_os_set_api(
    ecs_os_api_t *os_api)
{
    if (!ecs_os_api_initialized) {
        ecs_os_api = *os_api;
        ecs_os_api_initialized = true;
    }
}

void ecs_os_init(void)
{
    if (!ecs_os_api_initialized) {
        ecs_os_set_api_defaults();
    }
    
    if (!(ecs_os_api_init_count ++)) {
        if (ecs_os_api.init_) {
            ecs_os_api.init_();
        }
    }
}

void ecs_os_fini(void) {
    if (!--ecs_os_api_init_count) {
        if (ecs_os_api.fini_) {
            ecs_os_api.fini_();
        }
    }
}

#if !defined(ECS_TARGET_WINDOWS) && !defined(ECS_TARGET_EM) && !defined(ECS_TARGET_ANDROID)
#include <execinfo.h>
#define ECS_BT_BUF_SIZE 100
static
void dump_backtrace(
    FILE *stream) 
{
    int nptrs;
    void *buffer[ECS_BT_BUF_SIZE];
    char **strings;

    nptrs = backtrace(buffer, ECS_BT_BUF_SIZE);

    strings = backtrace_symbols(buffer, nptrs);
    if (strings == NULL) {
        return;
    }

    for (int j = 3; j < nptrs; j++) {
        fprintf(stream, "%s\n", strings[j]);
    }

    free(strings);
}
#else
static
void dump_backtrace(
    FILE *stream)
{ 
    (void)stream;
}
#endif

static
void log_msg(
    int32_t level,
    const char *file, 
    int32_t line,  
    const char *msg)
{
    FILE *stream;
    if (level >= 0) {
        stream = stdout;
    } else {
        stream = stderr;
    }

    bool use_colors = ecs_os_api.flags_ & EcsOsApiLogWithColors;
    bool timestamp = ecs_os_api.flags_ & EcsOsApiLogWithTimeStamp;
    bool deltatime = ecs_os_api.flags_ & EcsOsApiLogWithTimeDelta;

    time_t now = 0;

    if (deltatime) {
        now = time(NULL);
        time_t delta = 0;
        if (ecs_os_api.log_last_timestamp_) {
            delta = now - ecs_os_api.log_last_timestamp_;
        }
        ecs_os_api.log_last_timestamp_ = (int64_t)now;

        if (delta) {
            if (delta < 10) {
                fputs(" ", stream);
            }
            if (delta < 100) {
                fputs(" ", stream);
            }
            char time_buf[20];
            ecs_os_sprintf(time_buf, "%u", (uint32_t)delta);
            fputs("+", stream);
            fputs(time_buf, stream);
            fputs(" ", stream);
        } else {
            fputs("     ", stream);
        }
    }

    if (timestamp) {
        if (!now) {
            now = time(NULL);
        }
        char time_buf[20];
        ecs_os_sprintf(time_buf, "%u", (uint32_t)now);
        fputs(time_buf, stream);
        fputs(" ", stream);
    }

    if (level >= 0) {
        if (level == 0) {
            if (use_colors) fputs(ECS_MAGENTA, stream);
        } else {
            if (use_colors) fputs(ECS_GREY, stream);
        }
        fputs("info", stream);
    } else if (level == -2) {
        if (use_colors) fputs(ECS_YELLOW, stream);
        fputs("warning", stream);
    } else if (level == -3) {
        if (use_colors) fputs(ECS_RED, stream);
        fputs("error", stream);
    } else if (level == -4) {
        if (use_colors) fputs(ECS_RED, stream);
        fputs("fatal", stream);
    }

    if (use_colors) fputs(ECS_NORMAL, stream);
    fputs(": ", stream);

    if (level >= 0) {
        if (ecs_os_api.log_indent_) {
            char indent[32];
            int i, indent_count = ecs_os_api.log_indent_;
            if (indent_count > 15) indent_count = 15;

            for (i = 0; i < indent_count; i ++) {
                indent[i * 2] = '|';
                indent[i * 2 + 1] = ' ';
            }

            if (ecs_os_api.log_indent_ != indent_count) {
                indent[i * 2 - 2] = '+';
            }

            indent[i * 2] = '\0';

            fputs(indent, stream);
        }
    }

    if (level < 0) {
        if (file) {
            const char *file_ptr = strrchr(file, '/');
            if (!file_ptr) {
                file_ptr = strrchr(file, '\\');
            }

            if (file_ptr) {
                file = file_ptr + 1;
            }

            fputs(file, stream);
            fputs(": ", stream);
        }

        if (line) {
            fprintf(stream, "%d: ", line);
        }
    }

    fputs(msg, stream);

    fputs("\n", stream);

    if (level == -4) {
        dump_backtrace(stream);
    }
}

void ecs_os_dbg(
    const char *file, 
    int32_t line, 
    const char *msg)
{
    if (ecs_os_api.log_) {
        ecs_os_api.log_(1, file, line, msg);
    }
}

void ecs_os_trace(
    const char *file, 
    int32_t line, 
    const char *msg) 
{
    if (ecs_os_api.log_) {
        ecs_os_api.log_(0, file, line, msg);
    }
}

void ecs_os_warn(
    const char *file, 
    int32_t line, 
    const char *msg) 
{
    if (ecs_os_api.log_) {
        ecs_os_api.log_(-2, file, line, msg);
    }
}

void ecs_os_err(
    const char *file, 
    int32_t line, 
    const char *msg) 
{
    if (ecs_os_api.log_) {
        ecs_os_api.log_(-3, file, line, msg);
    }
}

void ecs_os_fatal(
    const char *file, 
    int32_t line, 
    const char *msg) 
{
    if (ecs_os_api.log_) {
        ecs_os_api.log_(-4, file, line, msg);
    }
}

static
void ecs_os_gettime(ecs_time_t *time) {
    ecs_assert(ecs_os_has_time() == true, ECS_MISSING_OS_API, NULL);
    
    uint64_t now = ecs_os_now();
    uint64_t sec = now / 1000000000;

    assert(sec < UINT32_MAX);
    assert((now - sec * 1000000000) < UINT32_MAX);

    time->sec = (uint32_t)sec;
    time->nanosec = (uint32_t)(now - sec * 1000000000);
}

static
void* ecs_os_api_malloc(ecs_size_t size) {
    ecs_os_api_malloc_count ++;
    ecs_assert(size > 0, ECS_INVALID_PARAMETER, NULL);
    return malloc((size_t)size);
}

static
void* ecs_os_api_calloc(ecs_size_t size) {
    ecs_os_api_calloc_count ++;
    ecs_assert(size > 0, ECS_INVALID_PARAMETER, NULL);
    return calloc(1, (size_t)size);
}

static
void* ecs_os_api_realloc(void *ptr, ecs_size_t size) {
    ecs_assert(size > 0, ECS_INVALID_PARAMETER, NULL);

    if (ptr) {
        ecs_os_api_realloc_count ++;
    } else {
        /* If not actually reallocing, treat as malloc */
        ecs_os_api_malloc_count ++; 
    }
    
    return realloc(ptr, (size_t)size);
}

static
void ecs_os_api_free(void *ptr) {
    if (ptr) {
        ecs_os_api_free_count ++;
    }
    free(ptr);
}

static
char* ecs_os_api_strdup(const char *str) {
    if (str) {
        int len = ecs_os_strlen(str);
        char *result = ecs_os_malloc(len + 1);
        ecs_assert(result != NULL, ECS_OUT_OF_MEMORY, NULL);
        ecs_os_strcpy(result, str);
        return result;
    } else {
        return NULL;
    }
}

/* Replace dots with underscores */
static
char *module_file_base(const char *module, char sep) {
    char *base = ecs_os_strdup(module);
    ecs_size_t i, len = ecs_os_strlen(base);
    for (i = 0; i < len; i ++) {
        if (base[i] == '.') {
            base[i] = sep;
        }
    }

    return base;
}

static
char* ecs_os_api_module_to_dl(const char *module) {
    ecs_strbuf_t lib = ECS_STRBUF_INIT;

    /* Best guess, use module name with underscores + OS library extension */
    char *file_base = module_file_base(module, '_');

#   if defined(ECS_TARGET_LINUX) || defined(ECS_TARGET_FREEBSD)
    ecs_strbuf_appendstr(&lib, "lib");
    ecs_strbuf_appendstr(&lib, file_base);
    ecs_strbuf_appendstr(&lib, ".so");
#   elif defined(ECS_TARGET_DARWIN)
    ecs_strbuf_appendstr(&lib, "lib");
    ecs_strbuf_appendstr(&lib, file_base);
    ecs_strbuf_appendstr(&lib, ".dylib");
#   elif defined(ECS_TARGET_WINDOWS)
    ecs_strbuf_appendstr(&lib, file_base);
    ecs_strbuf_appendstr(&lib, ".dll");
#   endif

    ecs_os_free(file_base);

    return ecs_strbuf_get(&lib);
}

static
char* ecs_os_api_module_to_etc(const char *module) {
    ecs_strbuf_t lib = ECS_STRBUF_INIT;

    /* Best guess, use module name with dashes + /etc */
    char *file_base = module_file_base(module, '-');

    ecs_strbuf_appendstr(&lib, file_base);
    ecs_strbuf_appendstr(&lib, "/etc");

    ecs_os_free(file_base);

    return ecs_strbuf_get(&lib);
}

void ecs_os_set_api_defaults(void)
{
    /* Don't overwrite if already initialized */
    if (ecs_os_api_initialized != 0) {
        return;
    }

    if (ecs_os_api_initializing != 0) {
        return;
    }

    ecs_os_api_initializing = true;
    
    /* Memory management */
    ecs_os_api.malloc_ = ecs_os_api_malloc;
    ecs_os_api.free_ = ecs_os_api_free;
    ecs_os_api.realloc_ = ecs_os_api_realloc;
    ecs_os_api.calloc_ = ecs_os_api_calloc;

    /* Strings */
    ecs_os_api.strdup_ = ecs_os_api_strdup;

    /* Time */
    ecs_os_api.get_time_ = ecs_os_gettime;

    /* Logging */
    ecs_os_api.log_ = log_msg;

    /* Modules */
    if (!ecs_os_api.module_to_dl_) {
        ecs_os_api.module_to_dl_ = ecs_os_api_module_to_dl;
    }

    if (!ecs_os_api.module_to_etc_) {
        ecs_os_api.module_to_etc_ = ecs_os_api_module_to_etc;
    }

    ecs_os_api.abort_ = abort;

#   ifdef FLECS_OS_API_IMPL
    /* Initialize defaults to OS API IMPL addon, but still allow for overriding
     * by the application */
    ecs_set_os_api_impl();
    ecs_os_api_initialized = false;
#   endif

    ecs_os_api_initializing = false;
}

bool ecs_os_has_heap(void) {
    return 
        (ecs_os_api.malloc_ != NULL) &&
        (ecs_os_api.calloc_ != NULL) &&
        (ecs_os_api.realloc_ != NULL) &&
        (ecs_os_api.free_ != NULL);
}

bool ecs_os_has_threading(void) {
    return
        (ecs_os_api.mutex_new_ != NULL) &&
        (ecs_os_api.mutex_free_ != NULL) &&
        (ecs_os_api.mutex_lock_ != NULL) &&
        (ecs_os_api.mutex_unlock_ != NULL) &&
        (ecs_os_api.cond_new_ != NULL) &&
        (ecs_os_api.cond_free_ != NULL) &&
        (ecs_os_api.cond_wait_ != NULL) &&
        (ecs_os_api.cond_signal_ != NULL) &&
        (ecs_os_api.cond_broadcast_ != NULL) &&
        (ecs_os_api.thread_new_ != NULL) &&
        (ecs_os_api.thread_join_ != NULL);   
}

bool ecs_os_has_time(void) {
    return 
        (ecs_os_api.get_time_ != NULL) &&
        (ecs_os_api.sleep_ != NULL) &&
        (ecs_os_api.now_ != NULL);
}

bool ecs_os_has_logging(void) {
    return (ecs_os_api.log_ != NULL);
}

bool ecs_os_has_dl(void) {
    return 
        (ecs_os_api.dlopen_ != NULL) &&
        (ecs_os_api.dlproc_ != NULL) &&
        (ecs_os_api.dlclose_ != NULL);  
}

bool ecs_os_has_modules(void) {
    return 
        (ecs_os_api.module_to_dl_ != NULL) &&
        (ecs_os_api.module_to_etc_ != NULL);
}

#if defined(ECS_TARGET_WINDOWS)
static char error_str[255];
#endif

const char* ecs_os_strerror(int err) {
#   if defined(ECS_TARGET_WINDOWS)
    strerror_s(error_str, 255, err);
    return error_str;
#   else
    return strerror(err);
#   endif
}


static
void flecs_query_compute_group_id(
    ecs_query_t *query,
    ecs_query_table_match_t *match)
{
    ecs_assert(match != NULL, ECS_INTERNAL_ERROR, NULL);

    if (query->group_by) {
        ecs_table_t *table = match->table;
        ecs_assert(table != NULL, ECS_INTERNAL_ERROR, NULL);

        match->group_id = query->group_by(query->world, table, 
            query->group_by_id, query->group_by_ctx);
    } else {
        match->group_id = 0;
    }
}

static
ecs_query_table_list_t* flecs_query_get_group(
    ecs_query_t *query,
    uint64_t group_id)
{
    return ecs_map_get(&query->groups, ecs_query_table_list_t, group_id);
}

static
ecs_query_table_list_t* flecs_query_ensure_group(
    ecs_query_t *query,
    uint64_t group_id)
{
    return ecs_map_ensure(&query->groups, ecs_query_table_list_t, group_id);
}

/* Find the last node of the group after which this group should be inserted */
static
ecs_query_table_node_t* flecs_query_find_group_insertion_node(
    ecs_query_t *query,
    uint64_t group_id)
{
    /* Grouping must be enabled */
    ecs_assert(query->group_by != NULL, ECS_INTERNAL_ERROR, NULL);

    ecs_map_iter_t it = ecs_map_iter(&query->groups);
    ecs_query_table_list_t *list, *closest_list = NULL;
    uint64_t id, closest_id = 0;

    /* Find closest smaller group id */
    while ((list = ecs_map_next(&it, ecs_query_table_list_t, &id))) {
        if (id >= group_id) {
            continue;
        }

        if (!list->last) {
            ecs_assert(list->first == NULL, ECS_INTERNAL_ERROR, NULL);
            continue;
        }

        if (!closest_list || ((group_id - id) < (group_id - closest_id))) {
            closest_id = id;
            closest_list = list;
        }
    }

    if (closest_list) {
        return closest_list->last;
    } else {
        return NULL; /* Group should be first in query */
    }
}

/* Initialize group with first node */
static
void flecs_query_create_group(
    ecs_query_t *query,
    ecs_query_table_node_t *node)
{
    ecs_query_table_match_t *match = node->match;
    uint64_t group_id = match->group_id;

    /* If query has grouping enabled & this is a new/empty group, find
     * the insertion point for the group */
    ecs_query_table_node_t *insert_after = flecs_query_find_group_insertion_node(
        query, group_id);

    if (!insert_after) {
        /* This group should appear first in the query list */
        ecs_query_table_node_t *query_first = query->list.first;
        if (query_first) {
            /* If this is not the first match for the query, insert before it */
            node->next = query_first;
            query_first->prev = node;
            query->list.first = node;
        } else {
            /* If this is the first match of the query, initialize its list */
            ecs_assert(query->list.last == NULL, ECS_INTERNAL_ERROR, NULL);
            query->list.first = node;
            query->list.last = node;
        }
    } else {
        ecs_assert(query->list.first != NULL, ECS_INTERNAL_ERROR, NULL);
        ecs_assert(query->list.last != NULL, ECS_INTERNAL_ERROR, NULL);

        /* This group should appear after another group */
        ecs_query_table_node_t *insert_before = insert_after->next;
        node->prev = insert_after;
        insert_after->next = node;
        node->next = insert_before;
        if (insert_before) {
            insert_before->prev = node;
        } else {
            ecs_assert(query->list.last == insert_after, 
                ECS_INTERNAL_ERROR, NULL);
                
            /* This group should appear last in the query list */
            query->list.last = node;
        }
    }
}

static
void flecs_query_remove_group(
    ecs_query_t *query,
    uint64_t group_id)
{
    ecs_map_remove(&query->groups, group_id);
}

/* Find the list the node should be part of */
static
ecs_query_table_list_t* flecs_query_get_node_list(
    ecs_query_t *query,
    ecs_query_table_node_t *node)
{
    ecs_query_table_match_t *match = node->match;
    if (query->group_by) {
        return flecs_query_get_group(query, match->group_id);
    } else {
        return &query->list;
    }
}

/* Find or create the list the node should be part of */
static
ecs_query_table_list_t* flecs_query_ensure_node_list(
    ecs_query_t *query,
    ecs_query_table_node_t *node)
{
    ecs_query_table_match_t *match = node->match;
    if (query->group_by) {
        return flecs_query_ensure_group(query, match->group_id);
    } else {
        return &query->list;
    }
}

/* Remove node from list */
static
void flecs_query_remove_table_node(
    ecs_query_t *query,
    ecs_query_table_node_t *node)
{
    ecs_query_table_node_t *prev = node->prev;
    ecs_query_table_node_t *next = node->next;

    ecs_assert(prev != node, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(next != node, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(!prev || prev != next, ECS_INTERNAL_ERROR, NULL);

    ecs_query_table_list_t *list = flecs_query_get_node_list(query, node);

    if (!list || !list->first) {
        /* If list contains no nodes, the node must be empty */
        ecs_assert(!list || list->last == NULL, ECS_INTERNAL_ERROR, NULL);
        ecs_assert(prev == NULL, ECS_INTERNAL_ERROR, NULL);
        ecs_assert(next == NULL, ECS_INTERNAL_ERROR, NULL);
        return;
    }

    ecs_assert(prev != NULL || query->list.first == node, 
        ECS_INTERNAL_ERROR, NULL);
    ecs_assert(next != NULL || query->list.last == node, 
        ECS_INTERNAL_ERROR, NULL);

    if (prev) {
        prev->next = next;
    }
    if (next) {
        next->prev = prev;
    }

    ecs_assert(list->count > 0, ECS_INTERNAL_ERROR, NULL);
    list->count --;

    if (query->group_by) {
        ecs_query_table_match_t *match = node->match;
        uint64_t group_id = match->group_id;

        /* Make sure query.list is updated if this is the first or last group */
        if (query->list.first == node) {
            ecs_assert(prev == NULL, ECS_INTERNAL_ERROR, NULL);
            query->list.first = next;
            prev = next;
        }
        if (query->list.last == node) {
            ecs_assert(next == NULL, ECS_INTERNAL_ERROR, NULL);
            query->list.last = prev;
            next = prev;
        }

        ecs_assert(query->list.count > 0, ECS_INTERNAL_ERROR, NULL);
        query->list.count --;

        /* Make sure group list only contains nodes that belong to the group */
        if (prev && prev->match->group_id != group_id) {
            /* The previous node belonged to another group */
            prev = next;
        }
        if (next && next->match->group_id != group_id) {
            /* The next node belonged to another group */
            next = prev;
        }

        /* Do check again, in case both prev & next belonged to another group */
        if (prev && prev->match->group_id != group_id) {
            /* There are no more matches left in this group */
            flecs_query_remove_group(query, group_id);
            list = NULL;
        }
    }

    if (list) {
        if (list->first == node) {
            list->first = next;
        }
        if (list->last == node) {
            list->last = prev;
        }
    }

    node->prev = NULL;
    node->next = NULL;

    query->match_count ++;
}

/* Add node to list */
static
void flecs_query_insert_table_node(
    ecs_query_t *query,
    ecs_query_table_node_t *node)
{
    /* Node should not be part of an existing list */
    ecs_assert(node->prev == NULL && node->next == NULL, 
        ECS_INTERNAL_ERROR, NULL);

    /* If this is the first match, activate system */
    if (!query->list.first && query->entity) {
        ecs_remove_id(query->world, query->entity, EcsEmpty);
    }

    flecs_query_compute_group_id(query, node->match);

    ecs_query_table_list_t *list = flecs_query_ensure_node_list(query, node);
    if (list->last) {
        ecs_assert(query->list.first != NULL, ECS_INTERNAL_ERROR, NULL);
        ecs_assert(query->list.last != NULL, ECS_INTERNAL_ERROR, NULL);
        ecs_assert(list->first != NULL, ECS_INTERNAL_ERROR, NULL);

        ecs_query_table_node_t *last = list->last;
        ecs_query_table_node_t *last_next = last->next;

        node->prev = last;
        node->next = last_next;
        last->next = node;

        if (last_next) {
            last_next->prev = node;
        }

        list->last = node;

        if (query->group_by) {
            /* Make sure to update query list if this is the last group */
            if (query->list.last == last) {
                query->list.last = node;
            }
        }
    } else {
        ecs_assert(list->first == NULL, ECS_INTERNAL_ERROR, NULL);

        list->first = node;
        list->last = node;

        if (query->group_by) {
            /* Initialize group with its first node */
            flecs_query_create_group(query, node);
        }
    }

    if (query->group_by) {
        query->list.count ++;
    }

    list->count ++;
    query->match_count ++;

    ecs_assert(node->prev != node, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(node->next != node, ECS_INTERNAL_ERROR, NULL);

    ecs_assert(list->first != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(list->last != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(list->last == node, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(query->list.first != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(query->list.last != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(query->list.first->prev == NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(query->list.last->next == NULL, ECS_INTERNAL_ERROR, NULL);
}

static
ecs_query_table_match_t* flecs_query_cache_add(
    ecs_query_table_t *elem)
{
    ecs_query_table_match_t *result = ecs_os_calloc_t(ecs_query_table_match_t);
    ecs_query_table_node_t *node = &result->node;

    node->match = result;
    if (!elem->first) {
        elem->first = result;
        elem->last = result;
    } else {
        ecs_assert(elem->last != NULL, ECS_INTERNAL_ERROR, NULL);
        elem->last->next_match = result;
        elem->last = result;
    }

    return result;
}

typedef struct {
    ecs_table_t *table;
    int32_t *dirty_state;
    int32_t column;
} table_dirty_state_t;

static
void flecs_query_get_dirty_state(
    ecs_query_t *query,
    ecs_query_table_match_t *match,
    int32_t term,
    table_dirty_state_t *out)
{
    ecs_world_t *world = query->world;
    ecs_entity_t subject = match->sources[term];
    int32_t column;

    if (!subject) {
        out->table = match->table;
        column = match->columns[term];
        if (column == -1) {
            column = 0;
        }
    } else {
        out->table = ecs_get_table(world, subject);
        column = -match->columns[term];
    }

    out->dirty_state = flecs_table_get_dirty_state(out->table);

    if (column) {
        out->column = ecs_table_type_to_storage_index(out->table, column - 1);
    } else {
        out->column = -1;
    }
}

/* Get match monitor. Monitors are used to keep track of whether components 
 * matched by the query in a table have changed. */
static
bool flecs_query_get_match_monitor(
    ecs_query_t *query,
    ecs_query_table_match_t *match)
{
    if (match->monitor) {
        return false;
    }

    int32_t *monitor = ecs_os_calloc_n(int32_t, query->filter.term_count + 1);

    /* Mark terms that don't need to be monitored. This saves time when reading
     * and/or updating the monitor. */
    const ecs_filter_t *f = &query->filter;
    int32_t i, t = -1, term_count = f->term_count_actual;
    table_dirty_state_t cur_dirty_state;

    for (i = 0; i < term_count; i ++) {
        if (t == f->terms[i].index) {
            if (monitor[t + 1] != -1) {
                continue;
            }
        }

        t = f->terms[i].index;
        monitor[t + 1] = -1;

        if (f->terms[i].inout != EcsIn && 
            f->terms[i].inout != EcsInOut &&
            f->terms[i].inout != EcsInOutDefault) {
            continue; /* If term isn't read, don't monitor */
        }

        int32_t column = match->columns[t];
        if (column == 0) {
            continue; /* Don't track terms that aren't matched */
        }

        flecs_query_get_dirty_state(query, match, t, &cur_dirty_state);
        if (cur_dirty_state.column == -1) {
            continue; /* Don't track terms that aren't stored */
        }

        monitor[t + 1] = 0;
    }

    match->monitor = monitor;

    query->flags |= EcsQueryHasMonitor;

    return true;
}

/* Synchronize match monitor with table dirty state */
static
void flecs_query_sync_match_monitor(
    ecs_query_t *query,
    ecs_query_table_match_t *match)
{
    ecs_assert(match != NULL, ECS_INTERNAL_ERROR, NULL);
    if (!match->monitor) {
        if (query->flags & EcsQueryHasMonitor) {
            flecs_query_get_match_monitor(query, match);
        } else {
            return;
        }
    }

    int32_t *monitor = match->monitor;
    ecs_table_t *table = match->table;
    int32_t *dirty_state = flecs_table_get_dirty_state(table);
    ecs_assert(dirty_state != NULL, ECS_INTERNAL_ERROR, NULL);
    table_dirty_state_t cur;

    monitor[0] = dirty_state[0]; /* Did table gain/lose entities */

    int32_t i, term_count = query->filter.term_count_actual;
    for (i = 0; i < term_count; i ++) {
        int32_t t = query->filter.terms[i].index;
        if (monitor[t + 1] == -1) {
            continue;
        }
                
        flecs_query_get_dirty_state(query, match, t, &cur);
        ecs_assert(cur.column != -1, ECS_INTERNAL_ERROR, NULL);
        monitor[t + 1] = cur.dirty_state[cur.column + 1];
    }
}

/* Check if single match term has changed */
static
bool flecs_query_check_match_monitor_term(
    ecs_query_t *query,
    ecs_query_table_match_t *match,
    int32_t term)
{
    ecs_assert(match != NULL, ECS_INTERNAL_ERROR, NULL);

    if (flecs_query_get_match_monitor(query, match)) {
        return true;
    }
    
    int32_t *monitor = match->monitor;
    ecs_table_t *table = match->table;
    int32_t *dirty_state = flecs_table_get_dirty_state(table);
    ecs_assert(dirty_state != NULL, ECS_INTERNAL_ERROR, NULL);
    table_dirty_state_t cur;

    int32_t state = monitor[term];
    if (state == -1) {
        return false;
    }

    if (!term) {
        return monitor[0] != dirty_state[0];
    }

    flecs_query_get_dirty_state(query, match, term - 1, &cur);
    ecs_assert(cur.column != -1, ECS_INTERNAL_ERROR, NULL);

    return monitor[term] != cur.dirty_state[cur.column + 1];
}

/* Check if any term for match has changed */
static
bool flecs_query_check_match_monitor(
    ecs_query_t *query,
    ecs_query_table_match_t *match)
{
    ecs_assert(match != NULL, ECS_INTERNAL_ERROR, NULL);

    if (flecs_query_get_match_monitor(query, match)) {
        return true;
    }

    int32_t *monitor = match->monitor;
    ecs_table_t *table = match->table;
    int32_t *dirty_state = flecs_table_get_dirty_state(table);
    ecs_assert(dirty_state != NULL, ECS_INTERNAL_ERROR, NULL);
    table_dirty_state_t cur;

    if (monitor[0] != dirty_state[0]) {
        return true;
    }

    ecs_filter_t *f = &query->filter;
    int32_t i, term_count = f->term_count_actual;
    for (i = 0; i < term_count; i ++) {
        ecs_term_t *term = &f->terms[i];
        int32_t t = term->index;
        if (monitor[t + 1] == -1) {
            continue;
        }

        flecs_query_get_dirty_state(query, match, t, &cur);
        ecs_assert(cur.column != -1, ECS_INTERNAL_ERROR, NULL);

        if (monitor[t + 1] != cur.dirty_state[cur.column + 1]) {
            return true;
        }
    }

    return false;
}

/* Check if any term for matched table has changed */
static
bool flecs_query_check_table_monitor(
    ecs_query_t *query,
    ecs_query_table_t *table,
    int32_t term)
{
    ecs_query_table_node_t *cur, *end = table->last->node.next;

    for (cur = &table->first->node; cur != end; cur = cur->next) {
        ecs_query_table_match_t *match = (ecs_query_table_match_t*)cur;
        if (term == -1) {
            if (flecs_query_check_match_monitor(query, match)) {
                return true;
            }
        } else {
            if (flecs_query_check_match_monitor_term(query, match, term)) {
                return true;
            } 
        }
    }

    return false;
}

static
bool flecs_query_check_query_monitor(
    ecs_query_t *query)
{
    ecs_table_cache_iter_t it;
    if (flecs_table_cache_iter(&query->cache, &it)) {
        ecs_query_table_t *qt;
        while ((qt = flecs_table_cache_next(&it, ecs_query_table_t))) {
            if (flecs_query_check_table_monitor(query, qt, -1)) {
                return true;
            }
        }
    }

    return false;
}

static
void flecs_query_init_query_monitors(
    ecs_query_t *query)
{
    ecs_query_table_node_t *cur = query->list.first;

    /* Ensure each match has a monitor */
    for (; cur != NULL; cur = cur->next) {
        ecs_query_table_match_t *match = (ecs_query_table_match_t*)cur;
        flecs_query_get_match_monitor(query, match);
    }
}

/* The group by function for cascade computes the tree depth for the table type.
 * This causes tables in the query cache to be ordered by depth, which ensures
 * breadth-first iteration order. */
static
uint64_t flecs_query_group_by_cascade(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_id_t id,
    void *ctx)
{
    (void)id;
    ecs_term_t *term = ctx;
    ecs_entity_t rel = term->src.trav;
    int32_t depth = flecs_relation_depth(world, rel, table);
    return flecs_ito(uint64_t, depth);
}

static
ecs_vector_t* flecs_query_add_ref(
    ecs_world_t *world,
    ecs_query_t *query,
    ecs_vector_t *references,
    ecs_term_t *term,
    ecs_entity_t component,
    ecs_entity_t entity,
    ecs_size_t size)
{
    ecs_assert(entity != 0, ECS_INTERNAL_ERROR, NULL);

    ecs_ref_t *ref = ecs_vector_add(&references, ecs_ref_t);
    ecs_term_id_t *src = &term->src;

    if (!(src->flags & EcsCascade)) {
        ecs_assert(entity != 0, ECS_INTERNAL_ERROR, NULL);
    }
    
    if (size) {
        *ref = ecs_ref_init_id(world, entity, component);
    } else {
        *ref = (ecs_ref_t){
            .entity = entity,
            .id = 0
        };
    }

    query->flags |= EcsQueryHasRefs;

    return references;
}

static
ecs_query_table_match_t* flecs_query_add_table_match(
    ecs_query_t *query,
    ecs_query_table_t *qt,
    ecs_table_t *table)
{
    ecs_filter_t *filter = &query->filter;
    int32_t term_count = filter->term_count;

    /* Add match for table. One table can have more than one match, if
     * the query contains wildcards. */
    ecs_query_table_match_t *qm = flecs_query_cache_add(qt);
    qm->table = table;
    qm->columns = ecs_os_malloc_n(int32_t, term_count);
    qm->ids = ecs_os_malloc_n(ecs_id_t, term_count);
    qm->sources = ecs_os_malloc_n(ecs_entity_t, term_count);
    qm->sizes = ecs_os_malloc_n(ecs_size_t, term_count);

    /* Insert match to iteration list if table is not empty */
    if (!table || ecs_table_count(table) != 0) {
        flecs_query_insert_table_node(query, &qm->node);
    }

    return qm;
}

static
void flecs_query_set_table_match(
    ecs_world_t *world,
    ecs_query_t *query,
    ecs_query_table_match_t *qm,
    ecs_table_t *table,
    ecs_iter_t *it)
{    
    ecs_filter_t *filter = &query->filter;
    int32_t i, term_count = filter->term_count;
    int32_t term_count_actual = filter->term_count_actual;
    ecs_term_t *terms = filter->terms;

    /* Reset resources in case this is an existing record */
    if (qm->sparse_columns) {
        ecs_vector_free(qm->sparse_columns);
        qm->sparse_columns = NULL;
    }
    if (qm->bitset_columns) {
        ecs_vector_free(qm->bitset_columns);
        qm->bitset_columns = NULL;
    }
    if (qm->references) {
        ecs_os_free(qm->references);
        qm->references = NULL;
    }

    ecs_os_memcpy_n(qm->columns, it->columns, int32_t, term_count_actual);
    ecs_os_memcpy_n(qm->ids, it->ids, ecs_id_t, term_count_actual);
    ecs_os_memcpy_n(qm->sources, it->sources, ecs_entity_t, term_count_actual);
    ecs_os_memcpy_n(qm->sizes, it->sizes, ecs_size_t, term_count_actual);

    /* Look for union & disabled terms */
    if (table) {
        if (table->flags & EcsTableHasUnion) {
            for (i = 0; i < term_count; i ++) {
                if (ecs_term_match_0(&terms[i])) {
                    continue;
                }

                ecs_id_t id = terms[i].id;
                if (ECS_HAS_ROLE(id, PAIR) && ECS_PAIR_SECOND(id) == EcsWildcard) {
                    continue;
                }
                
                int32_t actual_index = terms[i].index;
                int32_t column = it->columns[actual_index];
                if (column <= 0) {
                    continue;
                }

                ecs_id_t table_id = table->type.array[column - 1];
                if (ECS_PAIR_FIRST(table_id) != EcsUnion) {
                    continue;
                }

                flecs_switch_term_t *sc = ecs_vector_add(
                     &qm->sparse_columns, flecs_switch_term_t);
                sc->signature_column_index = actual_index;
                sc->sw_case = ECS_PAIR_SECOND(id);
                sc->sw_column = NULL;
                qm->ids[actual_index] = id;
            }
        }
        if (table->flags & EcsTableHasDisabled) {
            for (i = 0; i < term_count; i ++) {
                if (ecs_term_match_0(&terms[i])) {
                    continue;
                }

                int32_t actual_index = terms[i].index;
                ecs_id_t id = it->ids[actual_index];
                ecs_id_t bs_id = ECS_DISABLED | (id & ECS_COMPONENT_MASK);
                int32_t bs_index = ecs_search(world, table, bs_id, 0);
                if (bs_index != -1) {
                    flecs_bitset_term_t *bc = ecs_vector_add(
                        &qm->bitset_columns, flecs_bitset_term_t);
                    bc->column_index = bs_index;
                    bc->bs_column = NULL;
                }
            }
        }
    }

    /* Add references for substituted terms */
    ecs_vector_t *refs = NULL;
    for (i = 0; i < term_count; i ++) {
        ecs_term_t *term = &terms[i];
        if (!ecs_term_match_this(term)) {
            /* non-This terms are set during iteration */
            continue;
        }

        int32_t actual_index = terms[i].index;
        ecs_entity_t src = it->sources[actual_index];
        ecs_size_t size = 0;
        if (it->sizes) {
            size = it->sizes[actual_index];
        }
        if (src) {
            ecs_id_t id = it->ids[actual_index];
            ecs_assert(ecs_is_valid(world, src), ECS_INTERNAL_ERROR, NULL);

            if (id) {
                refs = flecs_query_add_ref(world, query, refs, term, id, src, size);

                /* Use column index to bind term and ref */
                if (qm->columns[actual_index] != 0) {
                    qm->columns[actual_index] = -ecs_vector_count(refs);
                }
            }
        }
    }
    if (refs) {
        int32_t count = ecs_vector_count(refs);
        ecs_ref_t *ptr = ecs_vector_first(refs, ecs_ref_t);
        qm->references = ecs_os_memdup_n(ptr, ecs_ref_t, count);
        ecs_vector_free(refs);
    }
}

/** Populate query cache with tables */
static
void flecs_query_match_tables(
    ecs_world_t *world,
    ecs_query_t *query)
{
    ecs_table_t *table = NULL;
    ecs_query_table_t *qt = NULL;

    ecs_iter_t it = ecs_filter_iter(world, &query->filter);
    ECS_BIT_SET(it.flags, EcsIterIsInstanced);
    ECS_BIT_SET(it.flags, EcsIterIsFilter);
    ECS_BIT_SET(it.flags, EcsIterEntityOptional);

    while (ecs_filter_next(&it)) {
        if ((table != it.table) || (!it.table && !qt)) {
            /* New table matched, add record to cache */
            qt = ecs_os_calloc_t(ecs_query_table_t);
            ecs_table_cache_insert(&query->cache, it.table, &qt->hdr);
            table = it.table;
        }

        ecs_query_table_match_t *qm = flecs_query_add_table_match(query, qt, table);
        flecs_query_set_table_match(world, query, qm, table, &it);
    }
}

static
bool flecs_query_match_table(
    ecs_world_t *world,
    ecs_query_t *query,
    ecs_table_t *table)
{
    if (!ecs_map_is_initialized(&query->cache.index)) {
        return false;
    }

    ecs_query_table_t *qt = NULL;
    int var_id = ecs_filter_find_this_var(&query->filter);
    if (var_id == -1) {
        /* If query doesn't match with This term, it can't match with tables */
        return false;
    }

    ecs_iter_t it = ecs_filter_iter(world, &query->filter);
    ECS_BIT_SET(it.flags, EcsIterIsInstanced);
    ECS_BIT_SET(it.flags, EcsIterIsFilter);
    ECS_BIT_SET(it.flags, EcsIterEntityOptional);
    ecs_iter_set_var_as_table(&it, var_id, table);

    while (ecs_filter_next(&it)) {
        ecs_assert(it.table == table, ECS_INTERNAL_ERROR, NULL);
        if (qt == NULL) {
            qt = ecs_os_calloc_t(ecs_query_table_t);
            ecs_table_cache_insert(&query->cache, it.table, &qt->hdr);
            table = it.table;
        }

        ecs_query_table_match_t *qm = flecs_query_add_table_match(query, qt, table);
        flecs_query_set_table_match(world, query, qm, table, &it);
    }

    return qt != NULL;
}

ECS_SORT_TABLE_WITH_COMPARE(_, flecs_query_sort_table_generic, order_by, static)

static
void flecs_query_sort_table(
    ecs_world_t *world,
    ecs_table_t *table,
    int32_t column_index,
    ecs_order_by_action_t compare,
    ecs_sort_table_action_t sort)
{
    ecs_data_t *data = &table->data;
    if (!ecs_storage_count(&data->entities)) {
        /* Nothing to sort */
        return;
    }

    int32_t count = flecs_table_data_count(data);
    if (count < 2) {
        return;
    }

    ecs_entity_t *entities = ecs_storage_first(&data->entities);

    void *ptr = NULL;
    int32_t size = 0;
    if (column_index != -1) {
        ecs_type_info_t *ti = table->type_info[column_index];
        ecs_column_t *column = &data->columns[column_index];
        size = ti->size;
        ptr = ecs_storage_first(column);
    }

    if (sort) {
        sort(world, table, entities, ptr, size, 0, count - 1, compare);
    } else {
        flecs_query_sort_table_generic(world, table, entities, ptr, size, 0, count - 1, compare);
    }
}

/* Helper struct for building sorted table ranges */
typedef struct sort_helper_t {
    ecs_query_table_match_t *match;
    ecs_entity_t *entities;
    const void *ptr;
    int32_t row;
    int32_t elem_size;
    int32_t count;
    bool shared;
} sort_helper_t;

static
const void* ptr_from_helper(
    sort_helper_t *helper)
{
    ecs_assert(helper->row < helper->count, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(helper->elem_size >= 0, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(helper->row >= 0, ECS_INTERNAL_ERROR, NULL);
    if (helper->shared) {
        return helper->ptr;
    } else {
        return ECS_ELEM(helper->ptr, helper->elem_size, helper->row);
    }
}

static
ecs_entity_t e_from_helper(
    sort_helper_t *helper)
{
    if (helper->row < helper->count) {
        return helper->entities[helper->row];
    } else {
        return 0;
    }
}

static
void flecs_query_build_sorted_table_range(
    ecs_query_t *query,
    ecs_query_table_list_t *list)
{
    ecs_world_t *world = query->world;
    ecs_entity_t id = query->order_by_component;
    ecs_order_by_action_t compare = query->order_by;
    
    if (!list->count) {
        return;
    }

    int to_sort = 0;

    sort_helper_t *helper = ecs_os_malloc_n(sort_helper_t, list->count);
    ecs_query_table_node_t *cur, *end = list->last->next;
    for (cur = list->first; cur != end; cur = cur->next) {
        ecs_query_table_match_t *match = cur->match;
        ecs_table_t *table = match->table;
        ecs_data_t *data = &table->data;

        ecs_assert(ecs_table_count(table) != 0, ECS_INTERNAL_ERROR, NULL);

        int32_t index = -1;
        if (id) {
            index = ecs_search(world, table->storage_table, id, 0);
        }

        if (index != -1) {
            ecs_type_info_t *ti = table->type_info[index];
            ecs_column_t *column = &data->columns[index];
            int32_t size = ti->size;
            helper[to_sort].ptr = ecs_storage_first(column);
            helper[to_sort].elem_size = size;
            helper[to_sort].shared = false;
        } else if (id) {
            /* Find component in prefab */
            ecs_entity_t base = 0;
            ecs_search_relation(world, table, 0, id, 
                EcsIsA, EcsUp, &base, 0, 0);

            /* If a base was not found, the query should not have allowed using
             * the component for sorting */
            ecs_assert(base != 0, ECS_INTERNAL_ERROR, NULL);

            const EcsComponent *cptr = ecs_get(world, id, EcsComponent);
            ecs_assert(cptr != NULL, ECS_INTERNAL_ERROR, NULL);

            helper[to_sort].ptr = ecs_get_id(world, base, id);
            helper[to_sort].elem_size = cptr->size;
            helper[to_sort].shared = true;
        } else {
            helper[to_sort].ptr = NULL;
            helper[to_sort].elem_size = 0;
            helper[to_sort].shared = false;
        }

        helper[to_sort].match = match;
        helper[to_sort].entities = ecs_storage_first(&data->entities);
        helper[to_sort].row = 0;
        helper[to_sort].count = ecs_table_count(table);
        to_sort ++;      
    }

    ecs_assert(to_sort != 0, ECS_INTERNAL_ERROR, NULL);

    bool proceed;
    do {
        int32_t j, min = 0;
        proceed = true;

        ecs_entity_t e1;
        while (!(e1 = e_from_helper(&helper[min]))) {
            min ++;
            if (min == to_sort) {
                proceed = false;
                break;
            }
        }

        if (!proceed) {
            break;
        }

        for (j = min + 1; j < to_sort; j++) {
            ecs_entity_t e2 = e_from_helper(&helper[j]);
            if (!e2) {
                continue;
            }

            const void *ptr1 = ptr_from_helper(&helper[min]);
            const void *ptr2 = ptr_from_helper(&helper[j]);

            if (compare(e1, ptr1, e2, ptr2) > 0) {
                min = j;
                e1 = e_from_helper(&helper[min]);
            }
        }

        sort_helper_t *cur_helper = &helper[min];
        if (!cur || cur->match != cur_helper->match) {
            cur = ecs_vector_add(&query->table_slices, ecs_query_table_node_t);
            ecs_assert(cur != NULL, ECS_INTERNAL_ERROR, NULL);
            cur->match = cur_helper->match;
            cur->offset = cur_helper->row;
            cur->count = 1;
        } else {
            cur->count ++;
        }

        cur_helper->row ++;
    } while (proceed);

    /* Iterate through the vector of slices to set the prev/next ptrs. This
     * can't be done while building the vector, as reallocs may occur */
    int32_t i, count = ecs_vector_count(query->table_slices);    
    ecs_query_table_node_t *nodes = ecs_vector_first(
        query->table_slices, ecs_query_table_node_t);
    for (i = 0; i < count; i ++) {
        nodes[i].prev = &nodes[i - 1];
        nodes[i].next = &nodes[i + 1];
    }

    nodes[0].prev = NULL;
    nodes[i - 1].next = NULL;

    ecs_os_free(helper);
}

static
void flecs_query_build_sorted_tables(
    ecs_query_t *query)
{
    ecs_vector_clear(query->table_slices);

    if (query->group_by) {
        /* Populate sorted node list in grouping order */
        ecs_query_table_node_t *cur = query->list.first;
        if (cur) {
            do {
                /* Find list for current group */
                ecs_query_table_match_t *match = cur->match;
                ecs_assert(match != NULL, ECS_INTERNAL_ERROR, NULL);
                uint64_t group_id = match->group_id;
                ecs_query_table_list_t *list = ecs_map_get(&query->groups, 
                    ecs_query_table_list_t, group_id);
                ecs_assert(list != NULL, ECS_INTERNAL_ERROR, NULL);

                /* Sort tables in current group */
                flecs_query_build_sorted_table_range(query, list);
                
                /* Find next group to sort */
                cur = list->last->next;
            } while (cur);
        }
    } else {
        flecs_query_build_sorted_table_range(query, &query->list);
    }
}

static
void flecs_query_sort_tables(
    ecs_world_t *world,
    ecs_query_t *query)
{
    ecs_order_by_action_t compare = query->order_by;
    if (!compare) {
        return;
    }

    ecs_sort_table_action_t sort = query->sort_table;
    
    ecs_entity_t order_by_component = query->order_by_component;
    int32_t i, order_by_term = -1;

    /* Find term that iterates over component (must be at least one) */
    if (order_by_component) {
        const ecs_filter_t *f = &query->filter;
        int32_t term_count = f->term_count_actual;
        for (i = 0; i < term_count; i ++) {
            ecs_term_t *term = &f->terms[i];
            if (!ecs_term_match_this(term)) {
                continue;
            }

            if (term->id == order_by_component) {
                order_by_term = i;
                break;
            }
        }

        ecs_assert(order_by_term != -1, ECS_INTERNAL_ERROR, NULL);
    }

    /* Iterate over non-empty tables. Don't bother with empty tables as they
     * have nothing to sort */

    bool tables_sorted = false;

    ecs_table_cache_iter_t it;
    ecs_query_table_t *qt;
    flecs_table_cache_iter(&query->cache, &it);

    while ((qt = flecs_table_cache_next(&it, ecs_query_table_t))) {
        ecs_table_t *table = qt->hdr.table;
        bool dirty = false;

        if (flecs_query_check_table_monitor(query, qt, 0)) {
            dirty = true;
        }

        int32_t column = -1;
        if (order_by_component) {
            if (flecs_query_check_table_monitor(query, qt, order_by_term + 1)) {
                dirty = true;
            }

            if (dirty) {
                column = -1;

                ecs_table_t *storage_table = table->storage_table;
                if (storage_table) {
                    column = ecs_search(world, storage_table, 
                        order_by_component, NULL);
                }

                if (column == -1) {
                    /* Component is shared, no sorting is needed */
                    dirty = false;
                }
            }
        }

        if (!dirty) {
            continue;
        }

        /* Something has changed, sort the table. Prefers using flecs_query_sort_table when available */
        flecs_query_sort_table(world, table, column, compare, sort);
        tables_sorted = true;
    }

    if (tables_sorted || query->match_count != query->prev_match_count) {
        flecs_query_build_sorted_tables(query);
        query->match_count ++; /* Increase version if tables changed */
    }
}

static
bool flecs_query_has_refs(
    ecs_query_t *query)
{
    ecs_term_t *terms = query->filter.terms;
    int32_t i, count = query->filter.term_count;
    for (i = 0; i < count; i ++) {
        if (terms[i].src.flags & (EcsUp | EcsIsEntity)) {
            return true;
        }
    }

    return false;
}

static
void flecs_query_for_each_component_monitor(
    ecs_world_t *world,
    ecs_query_t *query,
    void(*callback)(
        ecs_world_t* world,
        ecs_id_t id,
        ecs_query_t *query))
{
    ecs_term_t *terms = query->filter.terms;
    int32_t i, count = query->filter.term_count;

    for (i = 0; i < count; i++) {
        ecs_term_t *term = &terms[i];
        ecs_term_id_t *src = &term->src;

        if (src->flags & EcsUp) {
            callback(world, ecs_pair(src->trav, EcsWildcard), query);
            if (src->trav != EcsIsA) {
                callback(world, ecs_pair(EcsIsA, EcsWildcard), query);
            }
            callback(world, term->id, query);

        } else if (src->flags & EcsSelf && !ecs_term_match_this(term)) {
            callback(world, term->id, query);
        }
    }
}

static
bool flecs_query_is_term_id_supported(
    ecs_term_id_t *term_id)
{
    if (!(term_id->flags & EcsIsVariable)) {
        return true;
    }
    if (ecs_id_is_wildcard(term_id->id)) {
        return true;
    }
    return false;
}

static
void flecs_query_process_signature(
    ecs_world_t *world,
    ecs_query_t *query)
{
    ecs_term_t *terms = query->filter.terms;
    int32_t i, count = query->filter.term_count;

    for (i = 0; i < count; i ++) {
        ecs_term_t *term = &terms[i];
        ecs_term_id_t *first = &term->first;
        ecs_term_id_t *src = &term->src;
        ecs_term_id_t *second = &term->second;
        ecs_inout_kind_t inout = term->inout;

        bool is_src_ok = flecs_query_is_term_id_supported(src);
        bool is_first_ok = flecs_query_is_term_id_supported(first);
        bool is_second_ok = flecs_query_is_term_id_supported(second);

        (void)first;
        (void)second;
        (void)is_src_ok;
        (void)is_first_ok;
        (void)is_second_ok;

        /* Queries do not support named variables */
        ecs_check(is_src_ok || ecs_term_match_this(term),
            ECS_UNSUPPORTED, NULL);
        ecs_check(is_first_ok, ECS_UNSUPPORTED, NULL);
        ecs_check(is_second_ok,  ECS_UNSUPPORTED, NULL);

        if (inout != EcsIn) {
            query->flags |= EcsQueryHasOutColumns;
        }

        if (src->flags & EcsCascade) {
            /* Query can only have one cascade column */
            ecs_assert(query->cascade_by == 0, ECS_INVALID_PARAMETER, NULL);
            query->cascade_by = i + 1;
        }

        if ((src->flags & EcsTraverseFlags) == EcsSelf) {
            if (src->flags & EcsIsEntity) {
                flecs_add_flag(world, term->src.id, EcsEntityObserved);
            }
        }
    }

    query->flags |= (ecs_flags32_t)(flecs_query_has_refs(query) * EcsQueryHasRefs);

    if (!(query->flags & EcsQueryIsSubquery)) {
        flecs_query_for_each_component_monitor(world, query, flecs_monitor_register);
    }
error:
    return;
}

/** When a table becomes empty remove it from the query list, or vice versa. */
static
void flecs_query_update_table(
    ecs_query_t *query,
    ecs_table_t *table,
    bool empty)
{
    int32_t prev_count = ecs_query_table_count(query);
    ecs_table_cache_set_empty(&query->cache, table, empty);
    int32_t cur_count = ecs_query_table_count(query);

    if (prev_count != cur_count) {
        ecs_query_table_t *qt = ecs_table_cache_get(&query->cache, table);
        ecs_assert(qt != NULL, ECS_INTERNAL_ERROR, NULL);
        ecs_query_table_match_t *cur, *next;

        for (cur = qt->first; cur != NULL; cur = next) {
            next = cur->next_match;

            if (empty) {
                ecs_assert(ecs_table_count(table) == 0, 
                    ECS_INTERNAL_ERROR, NULL);

                flecs_query_remove_table_node(query, &cur->node);
            } else {
                ecs_assert(ecs_table_count(table) != 0, 
                    ECS_INTERNAL_ERROR, NULL);
                flecs_query_insert_table_node(query, &cur->node);
            }
        }
    }

    ecs_assert(cur_count || query->list.first == NULL, 
        ECS_INTERNAL_ERROR, NULL);
}

static
void flecs_query_add_subquery(
    ecs_world_t *world, 
    ecs_query_t *parent, 
    ecs_query_t *subquery) 
{
    ecs_query_t **elem = ecs_vector_add(&parent->subqueries, ecs_query_t*);
    *elem = subquery;

    ecs_table_cache_t *cache = &parent->cache;
    ecs_table_cache_iter_t it;
    ecs_query_table_t *qt;
    flecs_table_cache_all_iter(cache, &it);
    while ((qt = flecs_table_cache_next(&it, ecs_query_table_t))) {
        flecs_query_match_table(world, subquery, qt->hdr.table);
    }
}

static
void flecs_query_notify_subqueries(
    ecs_world_t *world,
    ecs_query_t *query,
    ecs_query_event_t *event)
{
    if (query->subqueries) {
        ecs_query_t **queries = ecs_vector_first(query->subqueries, ecs_query_t*);
        int32_t i, count = ecs_vector_count(query->subqueries);

        ecs_query_event_t sub_event = *event;
        sub_event.parent_query = query;

        for (i = 0; i < count; i ++) {
            ecs_query_t *sub = queries[i];
            flecs_query_notify(world, sub, &sub_event);
        }
    }
}

/* Remove table */
static
void flecs_query_table_match_free(
    ecs_query_t *query,
    ecs_query_table_t *elem,
    ecs_query_table_match_t *first)
{
    ecs_query_table_match_t *cur, *next;

    for (cur = first; cur != NULL; cur = next) {
        ecs_os_free(cur->columns);
        ecs_os_free(cur->ids);
        ecs_os_free(cur->sources);
        ecs_os_free(cur->sizes);
        ecs_os_free(cur->references);
        ecs_os_free(cur->sparse_columns);
        ecs_os_free(cur->bitset_columns);
        ecs_os_free(cur->monitor);

        if (!elem->hdr.empty) {
            flecs_query_remove_table_node(query, &cur->node);
        }

        next = cur->next_match;

        ecs_os_free(cur);
    }
}

static
void flecs_query_table_free(
    ecs_query_t *query,
    ecs_query_table_t *elem)
{
    flecs_query_table_match_free(query, elem, elem->first);
    ecs_os_free(elem);
}

static
void flecs_query_unmatch_table(
    ecs_query_t *query,
    ecs_table_t *table)
{
    ecs_query_table_t *qt = ecs_table_cache_remove(
        &query->cache, table, NULL);
    if (qt) {
        flecs_query_table_free(query, qt);
    }
}

/* Rematch system with tables after a change happened to a watched entity */
static
void flecs_query_rematch_tables(
    ecs_world_t *world,
    ecs_query_t *query,
    ecs_query_t *parent_query)
{
    ecs_iter_t it, parent_it;
    ecs_table_t *table = NULL;
    ecs_query_table_t *qt = NULL;
    ecs_query_table_match_t *qm = NULL;

    if (parent_query) {
        parent_it = ecs_query_iter(world, parent_query);
        it = ecs_filter_chain_iter(&parent_it, &query->filter);
    } else {
        it = ecs_filter_iter(world, &query->filter);
    }

    ECS_BIT_SET(it.flags, EcsIterIsInstanced);
    ECS_BIT_SET(it.flags, EcsIterIsFilter);
    ECS_BIT_SET(it.flags, EcsIterEntityOptional);

    int32_t rematch_count = ++ query->rematch_count;

    while (ecs_iter_next(&it)) {
        if ((table != it.table) || (!it.table && !qt)) {
            if (qm && qm->next_match) {
                flecs_query_table_match_free(query, qt, qm->next_match);
                qm->next_match = NULL;
            }

            table = it.table;

            qt = ecs_table_cache_get(&query->cache, table);
            if (!qt) {
                qt = ecs_os_calloc_t(ecs_query_table_t);
                ecs_table_cache_insert(&query->cache, table, &qt->hdr);
            }

            ecs_assert(qt->hdr.table == table, ECS_INTERNAL_ERROR, NULL);
            qt->rematch_count = rematch_count;
            qm = NULL;
        }
        if (!qm) {
            qm = qt->first;
        } else {
            qm = qm->next_match;
        }
        if (!qm) {
            qm = flecs_query_add_table_match(query, qt, table);
        }

        flecs_query_set_table_match(world, query, qm, table, &it);

        if (table && ecs_table_count(table) && query->group_by) {
            /* If grouping is enabled, make sure match is in the right place */
            flecs_query_remove_table_node(query, &qm->node);
            flecs_query_insert_table_node(query, &qm->node);
        }
    }

    if (qm && qm->next_match) {
        flecs_query_table_match_free(query, qt, qm->next_match);
        qm->next_match = NULL;
    }

    /* Iterate all tables in cache, remove ones that weren't just matched */
    ecs_table_cache_iter_t cache_it;
    if (flecs_table_cache_all_iter(&query->cache, &cache_it)) {
        while ((qt = flecs_table_cache_next(&cache_it, ecs_query_table_t))) {
            if (qt->rematch_count != rematch_count) {
                flecs_query_unmatch_table(query, qt->hdr.table);
            }
        }
    }
}

static
void flecs_query_remove_subquery(
    ecs_query_t *parent, 
    ecs_query_t *sub)
{
    ecs_assert(parent != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(sub != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(parent->subqueries != NULL, ECS_INTERNAL_ERROR, NULL);

    int32_t i, count = ecs_vector_count(parent->subqueries);
    ecs_query_t **sq = ecs_vector_first(parent->subqueries, ecs_query_t*);

    for (i = 0; i < count; i ++) {
        if (sq[i] == sub) {
            break;
        }
    }

    ecs_vector_remove(parent->subqueries, ecs_query_t*, i);
}

/* -- Private API -- */

void flecs_query_notify(
    ecs_world_t *world,
    ecs_query_t *query,
    ecs_query_event_t *event)
{
    bool notify = true;

    switch(event->kind) {
    case EcsQueryTableMatch:
        /* Creation of new table */
        if (flecs_query_match_table(world, query, event->table)) {
            if (query->subqueries) {
                flecs_query_notify_subqueries(world, query, event);
            }
        }
        notify = false;
        break;
    case EcsQueryTableUnmatch:
        /* Deletion of table */
        flecs_query_unmatch_table(query, event->table);
        break;
    case EcsQueryTableRematch:
        /* Rematch tables of query */
        flecs_query_rematch_tables(world, query, event->parent_query);
        break;        
    case EcsQueryOrphan:
        ecs_assert(query->flags & EcsQueryIsSubquery, ECS_INTERNAL_ERROR, NULL);
        query->flags |= EcsQueryIsOrphaned;
        query->parent = NULL;
        break;
    }

    if (notify) {
        flecs_query_notify_subqueries(world, query, event);
    }
}

static
void flecs_query_order_by(
    ecs_world_t *world,
    ecs_query_t *query,
    ecs_entity_t order_by_component,
    ecs_order_by_action_t order_by,
    ecs_sort_table_action_t action)
{
    ecs_check(query != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(!(query->flags & EcsQueryIsOrphaned), ECS_INVALID_PARAMETER, NULL);    

    query->order_by_component = order_by_component;
    query->order_by = order_by;
    query->sort_table = action;

    ecs_vector_free(query->table_slices);
    query->table_slices = NULL;

    flecs_query_sort_tables(world, query);  

    if (!query->table_slices) {
        flecs_query_build_sorted_tables(query);
    }
error:
    return;
}

static
void flecs_query_group_by(
    ecs_query_t *query,
    ecs_entity_t sort_component,
    ecs_group_by_action_t group_by)
{   
    /* Cannot change grouping once a query has been created */
    ecs_check(query->group_by_id == 0, ECS_INVALID_OPERATION, NULL);
    ecs_check(query->group_by == 0, ECS_INVALID_OPERATION, NULL);

    query->group_by_id = sort_component;
    query->group_by = group_by;
    ecs_map_init(&query->groups, ecs_query_table_list_t, 16);
error:
    return;
}

/* Implementation for iterable mixin */
static
void flecs_query_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_query_t);

    if (filter) {
        iter[1] = ecs_query_iter(world, (ecs_query_t*)poly);
        iter[0] = ecs_term_chain_iter(&iter[1], filter);
    } else {
        iter[0] = ecs_query_iter(world, (ecs_query_t*)poly);
    }
}

static
void flecs_query_on_event(
    ecs_iter_t *it)
{
    /* Because this is the observer::run callback, checking if this is event is
     * already handled is not done for us. */
    ecs_world_t *world = it->world;
    ecs_observer_t *o = it->ctx;
    if (o->last_event_id) {
        if (o->last_event_id[0] == world->event_id) {
            return;
        }
        o->last_event_id[0] = world->event_id;
    }

    ecs_query_t *query = o->ctx;
    ecs_table_t *table = it->table;
    ecs_assert(query != NULL, ECS_INTERNAL_ERROR, NULL);

    /* The observer isn't doing the matching because the query can do it more
     * efficiently by checking the table with the query cache. */
    if (ecs_table_cache_get(&query->cache, table) == NULL) {
        return;
    }

    ecs_entity_t event = it->event;
    if (event == EcsOnTableEmpty) {
        flecs_query_update_table(query, table, true);
    } else
    if (event == EcsOnTableFill) {
        flecs_query_update_table(query, table, false);
    }
}

static
void flecs_query_table_cache_free(
    ecs_query_t *query)
{
    ecs_table_cache_iter_t it;
    ecs_query_table_t *qt;

    if (flecs_table_cache_all_iter(&query->cache, &it)) {
        while ((qt = flecs_table_cache_next(&it, ecs_query_table_t))) {
            flecs_query_table_free(query, qt);
        }
    }

    ecs_table_cache_fini(&query->cache);
}

static
void flecs_query_fini(
    ecs_query_t *query)
{
    ecs_world_t *world = query->world;

    if (query->group_by_ctx_free) {
        if (query->group_by_ctx) {
            query->group_by_ctx_free(query->group_by_ctx);
        }
    }

    if ((query->flags & EcsQueryIsSubquery) &&
        !(query->flags & EcsQueryIsOrphaned))
    {
        flecs_query_remove_subquery(query->parent, query);
    }

    flecs_query_notify_subqueries(world, query, &(ecs_query_event_t){
        .kind = EcsQueryOrphan
    });

    flecs_query_for_each_component_monitor(world, query, 
        flecs_monitor_unregister);
    flecs_query_table_cache_free(query);

    ecs_map_fini(&query->groups);

    ecs_vector_free(query->subqueries);
    ecs_vector_free(query->table_slices);
    ecs_filter_fini(&query->filter);

    ecs_poly_free(query, ecs_query_t);
}


/* -- Public API -- */

ecs_query_t* ecs_query_init(
    ecs_world_t *world,
    const ecs_query_desc_t *desc)
{
    ecs_check(world != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_check(desc != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(desc->_canary == 0, ECS_INVALID_PARAMETER, NULL);
    ecs_check(!(world->flags & EcsWorldFini), ECS_INVALID_OPERATION, NULL);

    ecs_query_t *result = ecs_poly_new(ecs_query_t);
    ecs_observer_desc_t observer_desc = { .filter = desc->filter };
    ecs_entity_t entity = desc->entity;

    observer_desc.filter.flags = EcsFilterMatchEmptyTables;
    observer_desc.filter.storage = &result->filter;
    result->filter = ECS_FILTER_INIT;

    if (ecs_filter_init(world, &observer_desc.filter) == NULL) {
        goto error;
    }

    if (result->filter.term_count) {
        observer_desc.entity = entity;
        observer_desc.run = flecs_query_on_event;
        observer_desc.ctx = result;
        observer_desc.events[0] = EcsOnTableEmpty;
        observer_desc.events[1] = EcsOnTableFill;
        observer_desc.filter.flags |= EcsFilterIsFilter;

        /* ecs_filter_init could have moved away resources from the terms array
         * in the descriptor, so use the terms array from the filter. */
        observer_desc.filter.terms_buffer = result->filter.terms;
        observer_desc.filter.terms_buffer_count = result->filter.term_count;
        observer_desc.filter.expr = NULL; /* Already parsed */

        entity = ecs_observer_init(world, &observer_desc);
        if (!entity) {
            goto error;
        }
    }

    result->world = world;
    result->iterable.init = flecs_query_iter_init;
    result->dtor = (ecs_poly_dtor_t)flecs_query_fini;
    result->prev_match_count = -1;

    if (ecs_should_log_1()) {
        char *filter_expr = ecs_filter_str(world, &result->filter);
        ecs_dbg_1("#[green]query#[normal] [%s] created", 
            filter_expr ? filter_expr : "");
        ecs_os_free(filter_expr);
    }

    ecs_log_push_1();

    flecs_query_process_signature(world, result);

    /* Group before matching so we won't have to move tables around later */
    int32_t cascade_by = result->cascade_by;
    if (cascade_by) {
        flecs_query_group_by(result, result->filter.terms[cascade_by - 1].id,
            flecs_query_group_by_cascade);
        result->group_by_ctx = &result->filter.terms[cascade_by - 1];
    }

    if (desc->group_by) {
        /* Can't have a cascade term and group by at the same time, as cascade
         * uses the group_by mechanism */
        ecs_check(!result->cascade_by, ECS_INVALID_PARAMETER, NULL);
        flecs_query_group_by(result, desc->group_by_id, desc->group_by);
        result->group_by_ctx = desc->group_by_ctx;
        result->group_by_ctx_free = desc->group_by_ctx_free;
    }

    if (desc->parent != NULL) {
        result->flags |= EcsQueryIsSubquery;
    }

    /* If the query refers to itself, add the components that were queried for
     * to the query itself. */
    if (entity)  {
        int32_t t, term_count = result->filter.term_count;
        ecs_term_t *terms = result->filter.terms;

        for (t = 0; t < term_count; t ++) {
            ecs_term_t *term = &terms[t];
            if (term->src.id == entity) {
                ecs_add_id(world, entity, term->id);
            }
        }        
    }

    if (!entity) {
        entity = ecs_new_id(world);
    }

    EcsPoly *poly = ecs_poly_bind(world, entity, ecs_query_t);
    poly->poly = result;
    result->entity = entity;

    /* Ensure that while initially populating the query with tables, they are
     * in the right empty/non-empty list. This ensures the query won't miss
     * empty/non-empty events for tables that are currently out of sync, but
     * change back to being in sync before processing pending events. */
    ecs_run_aperiodic(world, EcsAperiodicEmptyTables);

    ecs_table_cache_init(&result->cache);

    if (!desc->parent) {
        flecs_query_match_tables(world, result);
    } else {
        flecs_query_add_subquery(world, desc->parent, result);
        result->parent = desc->parent;
    }

    if (desc->order_by) {
        flecs_query_order_by(
            world, result, desc->order_by_component, desc->order_by,
            desc->sort_table);
    }

    if (!ecs_query_table_count(result) && result->filter.term_count) {
        ecs_add_id(world, entity, EcsEmpty);
    }

    ecs_poly_modified(world, entity, ecs_query_t);

    ecs_log_pop_1();

    return result;
error:
    if (result) {
        ecs_filter_fini(&result->filter);
        ecs_os_free(result);
    }
    return NULL;
}

void ecs_query_fini(
    ecs_query_t *query)
{
    ecs_poly_assert(query, ecs_query_t);
    ecs_delete(query->world, query->entity);
}

const ecs_filter_t* ecs_query_get_filter(
    ecs_query_t *query)
{
    ecs_poly_assert(query, ecs_query_t);
    return &query->filter;
}

/* Create query iterator */
ecs_iter_t ecs_query_iter(
    const ecs_world_t *stage,
    ecs_query_t *query)
{
    ecs_poly_assert(query, ecs_query_t);
    ecs_check(!(query->flags & EcsQueryIsOrphaned),
        ECS_INVALID_PARAMETER, NULL);

    ecs_world_t *world = query->world;
    ecs_poly_assert(world, ecs_world_t);

    /* Process table events to ensure that the list of iterated tables doesn't
     * contain empty tables. */
    flecs_process_pending_tables(world);

    /* If query has order_by, apply sort */
    flecs_query_sort_tables(world, query);

    /* If monitors changed, do query rematching */
    if (!(world->flags & EcsWorldReadonly) && query->flags & EcsQueryHasRefs) {
        flecs_eval_component_monitors(world);
    }

    query->prev_match_count = query->match_count;

    /* Prepare iterator */

    int32_t table_count;
    if (query->table_slices) {
        table_count = ecs_vector_count(query->table_slices);
    } else {
        table_count = ecs_query_table_count(query);
    }

    ecs_query_iter_t it = {
        .query = query,
        .node = query->list.first
    };

    if (query->order_by && query->list.count) {
        it.node = ecs_vector_first(query->table_slices, ecs_query_table_node_t);
    }

    ecs_flags32_t flags = 0;
    ECS_BIT_COND(flags, EcsIterIsFilter, ECS_BIT_IS_SET(query->filter.flags, 
        EcsFilterIsFilter));
    ECS_BIT_COND(flags, EcsIterIsInstanced, ECS_BIT_IS_SET(query->filter.flags, 
        EcsFilterIsInstanced));

    ecs_iter_t result = {
        .real_world = world,
        .world = (ecs_world_t*)stage,
        .terms = query->filter.terms,
        .term_count = query->filter.term_count_actual,
        .table_count = table_count,
        .flags = flags,
        .priv.iter.query = it,
        .next = ecs_query_next,
    };

    ecs_filter_t *filter = &query->filter;
    if (filter->flags & EcsFilterMatchOnlyThis) {
        /* When the query only matches This terms, we can reuse the storage from
         * the cache to populate the iterator */
        flecs_iter_init(&result, flecs_iter_cache_ptrs);
    } else {
        /* Check if non-This terms (like singleton terms) still match */
        ecs_iter_t fit = flecs_filter_iter_w_flags(
            world, &query->filter, EcsIterIgnoreThis);
        if (!ecs_filter_next(&fit)) {
            /* No match, so return nothing */
            ecs_iter_fini(&result);
            goto noresults;
        }

        /* Initialize iterator with private storage for ids, ptrs, sizes and 
         * columns so we have a place to store the non-This data */
        flecs_iter_init(&result, flecs_iter_cache_ptrs | flecs_iter_cache_ids |
            flecs_iter_cache_columns | flecs_iter_cache_sizes);

        /* Copy the data */
        int32_t term_count = filter->term_count_actual;
        if (term_count) {
            if (result.ptrs) {
                ecs_os_memcpy_n(result.ptrs, fit.ptrs, void*, term_count);
            }
            ecs_os_memcpy_n(result.ids, fit.ids, ecs_id_t, term_count);
            ecs_os_memcpy_n(result.sizes, fit.sizes, ecs_size_t, term_count);
            ecs_os_memcpy_n(result.columns, fit.columns, int32_t, term_count);
        }
        ecs_iter_fini(&fit);
    }

    return result;
error:
noresults:
    return (ecs_iter_t) {
        .flags = EcsIterNoResults,
        .next = ecs_query_next
    };
}

static
int find_smallest_column(
    ecs_table_t *table,
    ecs_query_table_match_t *table_data,
    ecs_vector_t *sparse_columns)
{
    flecs_switch_term_t *sparse_column_array = 
        ecs_vector_first(sparse_columns, flecs_switch_term_t);
    int32_t i, count = ecs_vector_count(sparse_columns);
    int32_t min = INT_MAX, index = 0;

    for (i = 0; i < count; i ++) {
        /* The array with sparse queries for the matched table */
        flecs_switch_term_t *sparse_column = &sparse_column_array[i];

        /* Pointer to the switch column struct of the table */
        ecs_switch_t *sw = sparse_column->sw_column;

        /* If the sparse column pointer hadn't been retrieved yet, do it now */
        if (!sw) {
            /* Get the table column index from the signature column index */
            int32_t table_column_index = table_data->columns[
                sparse_column->signature_column_index];

            /* Translate the table column index to switch column index */
            table_column_index -= table->sw_offset;
            ecs_assert(table_column_index >= 1, ECS_INTERNAL_ERROR, NULL);

            /* Get the sparse column */
            ecs_data_t *data = &table->data;
            sw = sparse_column->sw_column = 
                &data->sw_columns[table_column_index - 1];
        }

        /* Find the smallest column */
        int32_t case_count = flecs_switch_case_count(sw, sparse_column->sw_case);
        if (case_count < min) {
            min = case_count;
            index = i + 1;
        }
    }

    return index;
}

typedef struct {
    int32_t first;
    int32_t count;
} query_iter_cursor_t;

static
int sparse_column_next(
    ecs_table_t *table,
    ecs_query_table_match_t *matched_table,
    ecs_vector_t *sparse_columns,
    ecs_query_iter_t *iter,
    query_iter_cursor_t *cur,
    bool filter)
{
    bool first_iteration = false;
    int32_t sparse_smallest;

    if (!(sparse_smallest = iter->sparse_smallest)) {
        sparse_smallest = iter->sparse_smallest = find_smallest_column(
            table, matched_table, sparse_columns);
        first_iteration = true;
    }

    sparse_smallest -= 1;

    flecs_switch_term_t *columns = ecs_vector_first(
        sparse_columns, flecs_switch_term_t);
    flecs_switch_term_t *column = &columns[sparse_smallest];
    ecs_switch_t *sw, *sw_smallest = column->sw_column;
    ecs_entity_t case_smallest = column->sw_case;

    /* Find next entity to iterate in sparse column */
    int32_t first, sparse_first = iter->sparse_first;

    if (!filter) {
        if (first_iteration) {
            first = flecs_switch_first(sw_smallest, case_smallest);
        } else {
            first = flecs_switch_next(sw_smallest, sparse_first);
        }
    } else {
        int32_t cur_first = cur->first, cur_count = cur->count;
        first = cur_first;
        while (flecs_switch_get(sw_smallest, first) != case_smallest) {
            first ++;
            if (first >= (cur_first + cur_count)) {
                first = -1;
                break;
            }
        }
    }

    if (first == -1) {
        goto done;
    }

    /* Check if entity matches with other sparse columns, if any */
    int32_t i, count = ecs_vector_count(sparse_columns);
    do {
        for (i = 0; i < count; i ++) {
            if (i == sparse_smallest) {
                /* Already validated this one */
                continue;
            }

            column = &columns[i];
            sw = column->sw_column;

            if (flecs_switch_get(sw, first) != column->sw_case) {
                first = flecs_switch_next(sw_smallest, first);
                if (first == -1) {
                    goto done;
                }
            }
        }
    } while (i != count);

    cur->first = iter->sparse_first = first;
    cur->count = 1;

    return 0;
done:
    /* Iterated all elements in the sparse list, we should move to the
     * next matched table. */
    iter->sparse_smallest = 0;
    iter->sparse_first = 0;

    return -1;
}

#define BS_MAX ((uint64_t)0xFFFFFFFFFFFFFFFF)

static
int bitset_column_next(
    ecs_table_t *table,
    ecs_vector_t *bitset_columns,
    ecs_query_iter_t *iter,
    query_iter_cursor_t *cur)
{
    /* Precomputed single-bit test */
    static const uint64_t bitmask[64] = {
    (uint64_t)1 << 0, (uint64_t)1 << 1, (uint64_t)1 << 2, (uint64_t)1 << 3,
    (uint64_t)1 << 4, (uint64_t)1 << 5, (uint64_t)1 << 6, (uint64_t)1 << 7,
    (uint64_t)1 << 8, (uint64_t)1 << 9, (uint64_t)1 << 10, (uint64_t)1 << 11,
    (uint64_t)1 << 12, (uint64_t)1 << 13, (uint64_t)1 << 14, (uint64_t)1 << 15,
    (uint64_t)1 << 16, (uint64_t)1 << 17, (uint64_t)1 << 18, (uint64_t)1 << 19,
    (uint64_t)1 << 20, (uint64_t)1 << 21, (uint64_t)1 << 22, (uint64_t)1 << 23,
    (uint64_t)1 << 24, (uint64_t)1 << 25, (uint64_t)1 << 26, (uint64_t)1 << 27,  
    (uint64_t)1 << 28, (uint64_t)1 << 29, (uint64_t)1 << 30, (uint64_t)1 << 31,
    (uint64_t)1 << 32, (uint64_t)1 << 33, (uint64_t)1 << 34, (uint64_t)1 << 35,  
    (uint64_t)1 << 36, (uint64_t)1 << 37, (uint64_t)1 << 38, (uint64_t)1 << 39,
    (uint64_t)1 << 40, (uint64_t)1 << 41, (uint64_t)1 << 42, (uint64_t)1 << 43,
    (uint64_t)1 << 44, (uint64_t)1 << 45, (uint64_t)1 << 46, (uint64_t)1 << 47,  
    (uint64_t)1 << 48, (uint64_t)1 << 49, (uint64_t)1 << 50, (uint64_t)1 << 51,
    (uint64_t)1 << 52, (uint64_t)1 << 53, (uint64_t)1 << 54, (uint64_t)1 << 55,  
    (uint64_t)1 << 56, (uint64_t)1 << 57, (uint64_t)1 << 58, (uint64_t)1 << 59,
    (uint64_t)1 << 60, (uint64_t)1 << 61, (uint64_t)1 << 62, (uint64_t)1 << 63
    };

    /* Precomputed test to verify if remainder of block is set (or not) */
    static const uint64_t bitmask_remain[64] = {
    BS_MAX, BS_MAX - (BS_MAX >> 63), BS_MAX - (BS_MAX >> 62),
    BS_MAX - (BS_MAX >> 61), BS_MAX - (BS_MAX >> 60), BS_MAX - (BS_MAX >> 59),
    BS_MAX - (BS_MAX >> 58), BS_MAX - (BS_MAX >> 57), BS_MAX - (BS_MAX >> 56), 
    BS_MAX - (BS_MAX >> 55), BS_MAX - (BS_MAX >> 54), BS_MAX - (BS_MAX >> 53), 
    BS_MAX - (BS_MAX >> 52), BS_MAX - (BS_MAX >> 51), BS_MAX - (BS_MAX >> 50), 
    BS_MAX - (BS_MAX >> 49), BS_MAX - (BS_MAX >> 48), BS_MAX - (BS_MAX >> 47), 
    BS_MAX - (BS_MAX >> 46), BS_MAX - (BS_MAX >> 45), BS_MAX - (BS_MAX >> 44), 
    BS_MAX - (BS_MAX >> 43), BS_MAX - (BS_MAX >> 42), BS_MAX - (BS_MAX >> 41), 
    BS_MAX - (BS_MAX >> 40), BS_MAX - (BS_MAX >> 39), BS_MAX - (BS_MAX >> 38), 
    BS_MAX - (BS_MAX >> 37), BS_MAX - (BS_MAX >> 36), BS_MAX - (BS_MAX >> 35), 
    BS_MAX - (BS_MAX >> 34), BS_MAX - (BS_MAX >> 33), BS_MAX - (BS_MAX >> 32), 
    BS_MAX - (BS_MAX >> 31), BS_MAX - (BS_MAX >> 30), BS_MAX - (BS_MAX >> 29), 
    BS_MAX - (BS_MAX >> 28), BS_MAX - (BS_MAX >> 27), BS_MAX - (BS_MAX >> 26), 
    BS_MAX - (BS_MAX >> 25), BS_MAX - (BS_MAX >> 24), BS_MAX - (BS_MAX >> 23), 
    BS_MAX - (BS_MAX >> 22), BS_MAX - (BS_MAX >> 21), BS_MAX - (BS_MAX >> 20), 
    BS_MAX - (BS_MAX >> 19), BS_MAX - (BS_MAX >> 18), BS_MAX - (BS_MAX >> 17), 
    BS_MAX - (BS_MAX >> 16), BS_MAX - (BS_MAX >> 15), BS_MAX - (BS_MAX >> 14), 
    BS_MAX - (BS_MAX >> 13), BS_MAX - (BS_MAX >> 12), BS_MAX - (BS_MAX >> 11), 
    BS_MAX - (BS_MAX >> 10), BS_MAX - (BS_MAX >> 9), BS_MAX - (BS_MAX >> 8), 
    BS_MAX - (BS_MAX >> 7), BS_MAX - (BS_MAX >> 6), BS_MAX - (BS_MAX >> 5), 
    BS_MAX - (BS_MAX >> 4), BS_MAX - (BS_MAX >> 3), BS_MAX - (BS_MAX >> 2),
    BS_MAX - (BS_MAX >> 1)
    };

    int32_t i, count = ecs_vector_count(bitset_columns);
    flecs_bitset_term_t *columns = ecs_vector_first(
        bitset_columns, flecs_bitset_term_t);
    int32_t bs_offset = table->bs_offset;

    int32_t first = iter->bitset_first;
    int32_t last = 0;

    for (i = 0; i < count; i ++) {
        flecs_bitset_term_t *column = &columns[i];
        ecs_bitset_t *bs = columns[i].bs_column;

        if (!bs) {
            int32_t index = column->column_index;
            ecs_assert((index - bs_offset >= 0), ECS_INTERNAL_ERROR, NULL);
            bs = &table->data.bs_columns[index - bs_offset];
            columns[i].bs_column = bs;
        }
        
        int32_t bs_elem_count = bs->count;
        int32_t bs_block = first >> 6;
        int32_t bs_block_count = ((bs_elem_count - 1) >> 6) + 1;

        if (bs_block >= bs_block_count) {
            goto done;
        }

        uint64_t *data = bs->data;
        int32_t bs_start = first & 0x3F;

        /* Step 1: find the first non-empty block */
        uint64_t v = data[bs_block];
        uint64_t remain = bitmask_remain[bs_start];
        while (!(v & remain)) {
            /* If no elements are remaining, move to next block */
            if ((++bs_block) >= bs_block_count) {
                /* No non-empty blocks left */
                goto done;
            }

            bs_start = 0;
            remain = BS_MAX; /* Test the full block */
            v = data[bs_block];
        }

        /* Step 2: find the first non-empty element in the block */
        while (!(v & bitmask[bs_start])) {
            bs_start ++;

            /* Block was not empty, so bs_start must be smaller than 64 */
            ecs_assert(bs_start < 64, ECS_INTERNAL_ERROR, NULL);
        }
        
        /* Step 3: Find number of contiguous enabled elements after start */
        int32_t bs_end = bs_start, bs_block_end = bs_block;
        
        remain = bitmask_remain[bs_end];
        while ((v & remain) == remain) {
            bs_end = 0;
            bs_block_end ++;

            if (bs_block_end == bs_block_count) {
                break;
            }

            v = data[bs_block_end];
            remain = BS_MAX; /* Test the full block */
        }

        /* Step 4: find remainder of enabled elements in current block */
        if (bs_block_end != bs_block_count) {
            while ((v & bitmask[bs_end])) {
                bs_end ++;
            }
        }

        /* Block was not 100% occupied, so bs_start must be smaller than 64 */
        ecs_assert(bs_end < 64, ECS_INTERNAL_ERROR, NULL);

        /* Step 5: translate to element start/end and make sure that each column
         * range is a subset of the previous one. */
        first = bs_block * 64 + bs_start;
        int32_t cur_last = bs_block_end * 64 + bs_end;
        
        /* No enabled elements found in table */
        if (first == cur_last) {
            goto done;
        }

        /* If multiple bitsets are evaluated, make sure each subsequent range
         * is equal or a subset of the previous range */
        if (i) {
            /* If the first element of a subsequent bitset is larger than the
             * previous last value, start over. */
            if (first >= last) {
                i = -1;
                continue;
            }

            /* Make sure the last element of the range doesn't exceed the last
             * element of the previous range. */
            if (cur_last > last) {
                cur_last = last;
            }
        }

        last = cur_last;
        int32_t elem_count = last - first;

        /* Make sure last element doesn't exceed total number of elements in 
         * the table */
        if (elem_count > (bs_elem_count - first)) {
            elem_count = (bs_elem_count - first);
            if (!elem_count) {
                iter->bitset_first = 0;
                goto done;
            }
        }
        
        cur->first = first;
        cur->count = elem_count;
        iter->bitset_first = first;
    }
    
    /* Keep track of last processed element for iteration */ 
    iter->bitset_first = last;

    return 0;
done:
    iter->sparse_smallest = 0;
    iter->sparse_first = 0;
    return -1;
}

static
void mark_columns_dirty(
    ecs_query_t *query,
    ecs_query_table_match_t *table_data)
{
    ecs_table_t *table = table_data->table;

    if (table && table->dirty_state) {
        ecs_term_t *terms = query->filter.terms;
        int32_t i, count = query->filter.term_count_actual;
        for (i = 0; i < count; i ++) {
            ecs_term_t *term = &terms[i];
            int32_t ti = term->index;

            if (term->inout == EcsIn || term->inout == EcsInOutNone) {
                /* Don't mark readonly terms dirty */
                continue;
            }

            if (table_data->sources[ti] != 0) {
                /* Don't mark table dirty if term is not from the table */
                continue;
            }

            int32_t index = table_data->columns[ti];
            if (index <= 0) {
                /* If term is not set, there's nothing to mark dirty */
                continue;
            }

            /* Potential candidate for marking table dirty, if a component */
            int32_t storage_index = ecs_table_type_to_storage_index(
                table, index - 1);
            if (storage_index >= 0) {
                table->dirty_state[storage_index + 1] ++;
            }
        }
    }
}

bool ecs_query_next(
    ecs_iter_t *it)
{
    ecs_check(it != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(it->next == ecs_query_next, ECS_INVALID_PARAMETER, NULL);

    if (flecs_iter_next_row(it)) {
        return true;
    }

    return flecs_iter_next_instanced(it, ecs_query_next_instanced(it));
error:
    return false;
}

bool ecs_query_next_instanced(
    ecs_iter_t *it)
{
    ecs_check(it != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(it->next == ecs_query_next, ECS_INVALID_PARAMETER, NULL);

    if (ECS_BIT_IS_SET(it->flags, EcsIterNoResults)) {
        goto done;
    }

    ECS_BIT_SET(it->flags, EcsIterIsValid);

    ecs_query_iter_t *iter = &it->priv.iter.query;
    ecs_query_t *query = iter->query;
    ecs_world_t *world = query->world;
    ecs_flags32_t flags = query->flags;
    const ecs_filter_t *filter = &query->filter;
    bool only_this = filter->flags & EcsFilterMatchOnlyThis;

    ecs_poly_assert(world, ecs_world_t);
    (void)world;

    query_iter_cursor_t cur;
    ecs_query_table_node_t *node, *next, *prev;
    if ((prev = iter->prev)) {
        /* Match has been iterated, update monitor for change tracking */
        if (flags & EcsQueryHasMonitor) {
            flecs_query_sync_match_monitor(query, prev->match);
        }
        if (flags & EcsQueryHasOutColumns) {
            mark_columns_dirty(query, prev->match);
        }
    }

    iter->skip_count = 0;

    flecs_iter_validate(it);

    for (node = iter->node; node != NULL; node = next) {     
        ecs_query_table_match_t *match = node->match;
        ecs_table_t *table = match->table;

        next = node->next;

        if (table) {
            cur.first = node->offset;
            cur.count = node->count;
            if (!cur.count) {
                cur.count = ecs_table_count(table);

                /* List should never contain empty tables */
                ecs_assert(cur.count != 0, ECS_INTERNAL_ERROR, NULL);
            }

            ecs_vector_t *bitset_columns = match->bitset_columns;
            ecs_vector_t *sparse_columns = match->sparse_columns;

            if (bitset_columns || sparse_columns) {
                bool found = false;

                do {
                    found = false;

                    if (bitset_columns) {
                        if (bitset_column_next(table, bitset_columns, iter, 
                            &cur) == -1) 
                        {
                            /* No more enabled components for table */
                            iter->bitset_first = 0;
                            break;
                        } else {
                            found = true;
                            next = node;
                        }
                    }

                    if (sparse_columns) {
                        if (sparse_column_next(table, match,
                            sparse_columns, iter, &cur, found) == -1)
                        {
                            /* No more elements in sparse column */
                            if (found) {
                                /* Try again */
                                next = node->next;
                                found = false;
                            } else {
                                /* Nothing found */
                                iter->bitset_first = 0;
                                break;
                            }
                        } else {
                            found = true;
                            next = node;
                            iter->bitset_first = cur.first + cur.count;
                        }
                    }
                } while (!found);

                if (!found) {
                    continue;
                }
            }
        } else {
            cur.count = 0;
            cur.first = 0;
        }

        if (only_this) {
            /* If query has only This terms, reuse cache storage */
            it->ids = match->ids;
            it->columns = match->columns;
            it->sizes = match->sizes;
        } else {
            /* If query has non-This terms make sure not to overwrite them */
            int32_t t, term_count = filter->term_count;
            for (t = 0; t < term_count; t ++) {
                ecs_term_t *term = &filter->terms[t];
                if (!ecs_term_match_this(term)) {
                    continue;
                }

                int32_t actual_index = term->index;
                it->ids[actual_index] = match->ids[actual_index];
                it->columns[actual_index] = match->columns[actual_index];
                it->sizes[actual_index] = match->sizes[actual_index];
            }
        }

        it->sources = match->sources;
        it->references = match->references;
        it->instance_count = 0;

        flecs_iter_populate_data(world, it, match->table, cur.first, cur.count, 
            it->ptrs, NULL);

        iter->node = next;
        iter->prev = node;
        goto yield;
    }

done:
error:
    ecs_iter_fini(it);
    return false;
    
yield:
    return true;  
}

bool ecs_query_changed(
    ecs_query_t *query,
    const ecs_iter_t *it)
{
    if (it) {
        ecs_check(it->next == ecs_query_next, ECS_INVALID_PARAMETER, NULL);
        ecs_check(ECS_BIT_IS_SET(it->flags, EcsIterIsValid), 
            ECS_INVALID_PARAMETER, NULL);

        ecs_query_table_match_t *qt = 
            (ecs_query_table_match_t*)it->priv.iter.query.prev;
        ecs_assert(qt != NULL, ECS_INVALID_PARAMETER, NULL);

        if (!query) {
            query = it->priv.iter.query.query;
        } else {
            ecs_check(query == it->priv.iter.query.query, 
                ECS_INVALID_PARAMETER, NULL);
        }

        ecs_check(query != NULL, ECS_INVALID_PARAMETER, NULL);
        ecs_poly_assert(query, ecs_query_t);

        flecs_process_pending_tables(it->real_world);

        return flecs_query_check_match_monitor(query, qt);
    }

    ecs_poly_assert(query, ecs_query_t);
    ecs_check(!(query->flags & EcsQueryIsOrphaned), 
        ECS_INVALID_PARAMETER, NULL);

    flecs_process_pending_tables(query->world);

    if (!(query->flags & EcsQueryHasMonitor)) {
        query->flags |= EcsQueryHasMonitor;
        flecs_query_init_query_monitors(query);
        return true; /* Monitors didn't exist yet */
    }

    if (query->match_count != query->prev_match_count) {
        return true;
    }

    return flecs_query_check_query_monitor(query);
error:
    return false;
}

void ecs_query_skip(
    ecs_iter_t *it)
{
    ecs_assert(it->next == ecs_query_next, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(ECS_BIT_IS_SET(it->flags, EcsIterIsValid), 
        ECS_INVALID_PARAMETER, NULL);

    if (it->instance_count > it->count) {
        it->priv.iter.query.skip_count ++;
        if (it->priv.iter.query.skip_count == it->instance_count) {
            /* For non-instanced queries, make sure all entities are skipped */
            it->priv.iter.query.prev = NULL;
        }
    } else {
        it->priv.iter.query.prev = NULL;
    }
}

bool ecs_query_orphaned(
    ecs_query_t *query)
{
    ecs_poly_assert(query, ecs_query_t);
    return query->flags & EcsQueryIsOrphaned;
}

char* ecs_query_str(
    const ecs_query_t *query)
{
    return ecs_filter_str(query->world, &query->filter);
}

int32_t ecs_query_table_count(
    const ecs_query_t *query)
{
    ecs_run_aperiodic(query->world, EcsAperiodicEmptyTables);
    return query->cache.tables.count;
}

int32_t ecs_query_empty_table_count(
    const ecs_query_t *query)
{
    ecs_run_aperiodic(query->world, EcsAperiodicEmptyTables);
    return query->cache.empty_tables.count;
}

int32_t ecs_query_entity_count(
    const ecs_query_t *query)
{
    ecs_run_aperiodic(query->world, EcsAperiodicEmptyTables);
    
    int32_t result = 0;
    ecs_table_cache_hdr_t *cur, *last = query->cache.tables.last;
    if (!last) {
        return 0;
    }

    for (cur = query->cache.tables.first; cur != NULL; cur = cur->next) {
        result += ecs_table_count(cur->table);
    }

    return result;
}

#include <stdio.h>

/* Marker object used to differentiate a component vs. a tag edge */
static ecs_table_diff_t ecs_table_edge_is_component;

/* Id sequence (type) utilities */

static
uint64_t flecs_type_hash(const void *ptr) {
    const ecs_type_t *type = ptr;
    ecs_id_t *ids = type->array;
    int32_t count = type->count;
    return flecs_hash(ids, count * ECS_SIZEOF(ecs_id_t));
}

static
int flecs_type_compare(const void *ptr_1, const void *ptr_2) {
    const ecs_type_t *type_1 = ptr_1;
    const ecs_type_t *type_2 = ptr_2;

    int32_t count_1 = type_1->count;
    int32_t count_2 = type_2->count;

    if (count_1 != count_2) {
        return (count_1 > count_2) - (count_1 < count_2);
    }

    const ecs_id_t *ids_1 = type_1->array;
    const ecs_id_t *ids_2 = type_2->array;
    int result = 0;
    
    int32_t i;
    for (i = 0; !result && (i < count_1); i ++) {
        ecs_id_t id_1 = ids_1[i];
        ecs_id_t id_2 = ids_2[i];
        result = (id_1 > id_2) - (id_1 < id_2);
    }

    return result;
}

void flecs_table_hashmap_init(ecs_hashmap_t *hm) {
    flecs_hashmap_init(hm, ecs_type_t, ecs_table_t*, 
        flecs_type_hash, flecs_type_compare);
}

/* Find location where to insert id into type */
static
int flecs_type_find_insert(
    const ecs_type_t *type,
    int32_t offset,
    ecs_id_t to_add)
{
    ecs_id_t *array = type->array;
    int32_t i, count = type->count;

    for (i = offset; i < count; i ++) {
        ecs_id_t id = array[i];
        if (id == to_add) {
            return -1;
        }
        if (id > to_add) {
            return i;
        }
    }
    return i;
}

/* Find location of id in type */
static
int flecs_type_find(
    const ecs_type_t *type,
    ecs_id_t id)
{
    ecs_id_t *array = type->array;
    int32_t i, count = type->count;

    for (i = 0; i < count; i ++) {
        ecs_id_t cur = array[i];
        if (ecs_id_match(cur, id)) {
            return i;
        }
        if (cur > id) {
            return -1;
        }
    }

    return -1;
}

/* Count number of matching ids */
static
int flecs_type_count_matches(
    const ecs_type_t *type,
    ecs_id_t wildcard,
    int32_t offset)
{
    ecs_id_t *array = type->array;
    int32_t i = offset, count = type->count;

    for (; i < count; i ++) {
        ecs_id_t cur = array[i];
        if (!ecs_id_match(cur, wildcard)) {
            break;
        }
    }

    return i - offset;
}

/* Create type from source type with id */
static
int flecs_type_new_with(
    ecs_type_t *dst,
    const ecs_type_t *src,
    ecs_id_t with)
{
    ecs_id_t *src_array = src->array;
    int32_t at = flecs_type_find_insert(src, 0, with);
    if (at == -1) {
        return -1;
    }

    int32_t dst_count = src->count + 1;
    ecs_id_t *dst_array = ecs_os_malloc_n(ecs_id_t, dst_count);
    dst->count = dst_count;
    dst->array = dst_array;

    if (at) {
        ecs_os_memcpy_n(dst_array, src_array, ecs_id_t, at);
    }

    int32_t remain = src->count - at;
    if (remain) {
        ecs_os_memcpy_n(&dst_array[at + 1], &src_array[at], ecs_id_t, remain);
    }

    dst_array[at] = with;

    return 0;
}

/* Create type from source type without ids matching wildcard */
static
int flecs_type_new_filtered(
    ecs_type_t *dst,
    const ecs_type_t *src,
    ecs_id_t wildcard,
    int32_t at)
{
    *dst = flecs_type_copy(src);
    ecs_id_t *dst_array = dst->array;
    ecs_id_t *src_array = src->array;
    if (at) {
        ecs_os_memcpy_n(dst_array, src_array, ecs_id_t, at);
    }

    int32_t i = at + 1, w = at, count = src->count;
    for (; i < count; i ++) {
        ecs_id_t id = src_array[i];
        if (!ecs_id_match(id, wildcard)) {
            dst_array[w] = id;
            w ++;
        }
    }

    dst->count = w;
    if (w != count) {
        dst->array = ecs_os_realloc_n(dst->array, ecs_id_t, w);
    }

    return 0;
}

/* Create type from source type without id */
static
int flecs_type_new_without(
    ecs_type_t *dst,
    const ecs_type_t *src,
    ecs_id_t without)
{
    ecs_id_t *src_array = src->array;
    int32_t count = 1, at = flecs_type_find(src, without);
    if (at == -1) {
        return -1;
    }

    int32_t src_count = src->count;
    if (src_count == 1) {
        dst->array = NULL;
        dst->count = 0;
        return 0;
    }

    if (ecs_id_is_wildcard(without)) {
        if (ECS_HAS_ROLE(without, PAIR)) {
            ecs_entity_t r = ECS_PAIR_FIRST(without);
            ecs_entity_t o = ECS_PAIR_SECOND(without);
            if (r == EcsWildcard && o != EcsWildcard) {
                return flecs_type_new_filtered(dst, src, without, at);
            }
        }
        count += flecs_type_count_matches(src, without, at + 1);
    }

    int32_t dst_count = src_count - count;
    dst->count = dst_count;
    if (!dst_count) {
        dst->array = NULL;
        return 0;
    }

    ecs_id_t *dst_array = ecs_os_malloc_n(ecs_id_t, dst_count);
    dst->array = dst_array;

    if (at) {
        ecs_os_memcpy_n(dst_array, src_array, ecs_id_t, at);
    }

    int32_t remain = dst_count - at;
    if (remain) {
        ecs_os_memcpy_n(
            &dst_array[at], &src_array[at + count], ecs_id_t, remain);
    }

    return 0;
}

/* Copy type */
ecs_type_t flecs_type_copy(
    const ecs_type_t *src)
{
    int32_t src_count = src->count;
    return (ecs_type_t) {
        .array = ecs_os_memdup_n(src->array, ecs_id_t, src_count),
        .count = src_count
    };
}

/* Free type */
void flecs_type_free(
    ecs_type_t *type)
{
    ecs_os_free(type->array);
}

/* Add to type */
static
void flecs_type_add(
    ecs_type_t *type,
    ecs_id_t add)
{
    ecs_type_t new_type;
    int res = flecs_type_new_with(&new_type, type, add);
    if (res != -1) {
        flecs_type_free(type);
        type->array = new_type.array;
        type->count = new_type.count;
    }
}

/* Graph edge utilities */

static
void table_diff_free(
    ecs_table_diff_t *diff) 
{
    ecs_os_free(diff->added.array);
    ecs_os_free(diff->removed.array);
    ecs_os_free(diff->on_set.array);
    ecs_os_free(diff->un_set.array);
    ecs_os_free(diff);
}

static
ecs_graph_edge_t* graph_edge_new(
    ecs_world_t *world)
{
    ecs_graph_edge_t *result = (ecs_graph_edge_t*)world->store.first_free;
    if (result) {
        world->store.first_free = result->hdr.next;
        ecs_os_zeromem(result);
    } else {
        result = ecs_os_calloc_t(ecs_graph_edge_t);
    }
    return result;
}

static
void graph_edge_free(
    ecs_world_t *world,
    ecs_graph_edge_t *edge)
{
    if (world->flags & EcsWorldFini) {
        ecs_os_free(edge);
    } else {
        edge->hdr.next = world->store.first_free;
        world->store.first_free = &edge->hdr;
    }
}

static
ecs_graph_edge_t* ensure_hi_edge(
    ecs_world_t *world,
    ecs_graph_edges_t *edges,
    ecs_id_t id)
{
    if (!ecs_map_is_initialized(&edges->hi)) {
        ecs_map_init(&edges->hi, ecs_graph_edge_t*, 1);
    }

    ecs_graph_edge_t **ep = ecs_map_ensure(&edges->hi, ecs_graph_edge_t*, id);
    ecs_graph_edge_t *edge = ep[0];
    if (edge) {
        return edge;
    }

    if (id < ECS_HI_COMPONENT_ID) {
        edge = &edges->lo[id];
    } else {
        edge = graph_edge_new(world);
    }

    ep[0] = edge;
    return edge;
}

static
ecs_graph_edge_t* ensure_edge(
    ecs_world_t *world,
    ecs_graph_edges_t *edges,
    ecs_id_t id)
{
    ecs_graph_edge_t *edge;
    
    if (id < ECS_HI_COMPONENT_ID) {
        if (!edges->lo) {
            edges->lo = ecs_os_calloc_n(ecs_graph_edge_t, ECS_HI_COMPONENT_ID);
        }
        edge = &edges->lo[id];
    } else {
        if (!ecs_map_is_initialized(&edges->hi)) {
            ecs_map_init(&edges->hi, ecs_graph_edge_t*, 1);
        }
        edge = ensure_hi_edge(world, edges, id);
    }

    return edge;
}

static
void disconnect_edge(
    ecs_world_t *world,
    ecs_id_t id,
    ecs_graph_edge_t *edge)
{
    ecs_assert(edge != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(edge->id == id, ECS_INTERNAL_ERROR, NULL);
    (void)id;

    /* Remove backref from destination table */
    ecs_graph_edge_hdr_t *next = edge->hdr.next;
    ecs_graph_edge_hdr_t *prev = edge->hdr.prev;

    if (next) {
        next->prev = prev;
    }
    if (prev) {
        prev->next = next;
    }

    /* Remove data associated with edge */
    ecs_table_diff_t *diff = edge->diff;
    if (diff && diff != &ecs_table_edge_is_component) {
        table_diff_free(diff);
    }

    /* If edge id is low, clear it from fast lookup array */
    if (id < ECS_HI_COMPONENT_ID) {
        ecs_os_memset_t(edge, 0, ecs_graph_edge_t);
    } else {
        graph_edge_free(world, edge);
    }
}

static
void remove_edge(
    ecs_world_t *world,
    ecs_graph_edges_t *edges,
    ecs_id_t id,
    ecs_graph_edge_t *edge)
{
    ecs_assert(edges != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(ecs_map_is_initialized(&edges->hi), ECS_INTERNAL_ERROR, NULL);
    disconnect_edge(world, id, edge);
    ecs_map_remove(&edges->hi, id);
}

static
void init_edges(
    ecs_graph_edges_t *edges)
{
    edges->lo = NULL;
    ecs_os_zeromem(&edges->hi);
}

static
void init_node(
    ecs_graph_node_t *node)
{
    init_edges(&node->add);
    init_edges(&node->remove);
}

bool flecs_table_records_update_empty(
    ecs_table_t *table)
{
    bool result = false;
    bool is_empty = ecs_table_count(table) == 0;

    int32_t i, count = table->record_count;
    for (i = 0; i < count; i ++) {
        ecs_table_record_t *tr = &table->records[i];
        ecs_table_cache_t *cache = tr->hdr.cache;
        result |= ecs_table_cache_set_empty(cache, table, is_empty);
    }

    return result;
}

static
void init_table(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_table_t *prev)
{
    table->type_info = NULL;
    table->flags = 0;
    table->dirty_state = NULL;
    table->lock = 0;
    table->refcount = 1;
    table->generation = 0;

    init_node(&table->node);

    flecs_table_init(world, table, prev);
}

static
ecs_table_t *create_table(
    ecs_world_t *world,
    ecs_type_t *type,
    flecs_hashmap_result_t table_elem,
    ecs_table_t *prev)
{
    ecs_table_t *result = flecs_sparse_add(&world->store.tables, ecs_table_t);
    ecs_assert(result != NULL, ECS_INTERNAL_ERROR, NULL);

    result->id = flecs_sparse_last_id(&world->store.tables);
    result->type = *type;

    if (ecs_should_log_2()) {
        char *expr = ecs_type_str(world, &result->type);
        ecs_dbg_2(
            "#[green]table#[normal] [%s] #[green]created#[reset] with id %d", 
            expr, result->id);
        ecs_os_free(expr);
    }

    ecs_log_push_2();

    /* Store table in table hashmap */
    *(ecs_table_t**)table_elem.value = result;

    /* Set keyvalue to one that has the same lifecycle as the table */
    *(ecs_type_t*)table_elem.key = result->type;

    init_table(world, result, prev);

    flecs_notify_queries(world, &(ecs_query_event_t) {
        .kind = EcsQueryTableMatch,
        .table = result
    });

    /* Update counters */
    world->info.table_count ++;
    world->info.table_record_count += result->record_count;
    world->info.table_storage_count += result->storage_count;
    world->info.empty_table_count ++;
    world->info.table_create_total ++;
    
    if (!result->storage_count) {
        world->info.tag_table_count ++;
    } else {
        world->info.trivial_table_count += !(result->flags & EcsTableIsComplex);
    }

    ecs_log_pop_2();

    return result;
}

static
ecs_table_t* find_or_create(
    ecs_world_t *world,
    ecs_type_t *type,
    bool own_type,
    ecs_table_t *prev)
{    
    ecs_poly_assert(world, ecs_world_t);   

    int32_t id_count = type->count;
    if (!id_count) {
        return &world->store.root;
    }

    ecs_table_t *table;
    flecs_hashmap_result_t elem = flecs_hashmap_ensure(
        &world->store.table_map, type, ecs_table_t*);
    if ((table = *(ecs_table_t**)elem.value)) {
        if (own_type) {
            flecs_type_free(type);
        }
        return table;
    }

    /* If we get here, table needs to be created which is only allowed when the
     * application is not currently in progress */
    ecs_assert(!(world->flags & EcsWorldReadonly), ECS_INTERNAL_ERROR, NULL);

    /* If we get here, the table has not been found, so create it. */
    if (own_type) {
        return create_table(world, type, elem, prev);
    }

    ecs_type_t copy = flecs_type_copy(type);
    return create_table(world, &copy, elem, prev);
}

static
void ids_append(
    ecs_type_t *ids,
    ecs_id_t id)
{
    ids->array = ecs_os_realloc_n(ids->array, ecs_id_t, ids->count + 1);
    ids->array[ids->count ++] = id;   
}

static
void diff_insert_isa(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_table_diff_t *base_diff,
    ecs_type_t *append_to,
    ecs_type_t *append_from,
    ecs_id_t add)
{
    ecs_entity_t base = ecs_pair_second(world, add);
    ecs_table_t *base_table = ecs_get_table(world, base);
    if (!base_table) {
        return;
    }

    ecs_type_t base_type = base_table->type;
    ecs_table_t *table_wo_base = base_table;

    /* If the table does not have a component from the base, it should
     * emit an OnSet event */
    ecs_id_t *ids = base_type.array;
    int32_t j, i, count = base_type.count;
    for (i = 0; i < count; i ++) {
        ecs_id_t id = ids[i];

        if (ECS_HAS_RELATION(id, EcsIsA)) {
            /* The base has an IsA relationship. Find table without the base, which
             * gives us the list of ids the current base inherits and doesn't
             * override. This saves us from having to recursively check for each
             * base in the hierarchy whether the component is overridden. */
            table_wo_base = flecs_table_traverse_remove(
                world, table_wo_base, &id, base_diff);

            /* Because we removed, the ids are stored in un_set vs. on_set */
            for (j = 0; j < append_from->count; j ++) {
                ecs_id_t base_id = append_from->array[j];
                /* We still have to make sure the id isn't overridden by the
                 * current table */
                if (ecs_search(world, table, base_id, NULL) == -1) {
                    ids_append(append_to, base_id);
                }
            }

            continue;
        }

        /* Identifiers are not inherited */
        if (ECS_HAS_RELATION(id, ecs_id(EcsIdentifier))) {
            continue;
        }

        if (!ecs_get_typeid(world, id)) {
            continue;
        }

        if (ecs_search(world, table, id, NULL) == -1) {
            ids_append(append_to, id);
        }
    }
}

static
void diff_insert_added_isa(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_table_diff_t *diff,
    ecs_id_t id)
{
    ecs_table_diff_t base_diff;
    diff_insert_isa(world, table, &base_diff, &diff->on_set, 
        &base_diff.un_set, id);
}

static
void diff_insert_removed_isa(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_table_diff_t *diff,
    ecs_id_t id)
{
    ecs_table_diff_t base_diff;
    diff_insert_isa(world, table, &base_diff, &diff->un_set, 
        &base_diff.un_set, id);
}

static
void diff_insert_added(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_table_diff_t *diff,
    ecs_id_t id)
{
    diff->added.array[diff->added.count ++] = id;

    if (ECS_HAS_RELATION(id, EcsIsA)) {
        diff_insert_added_isa(world, table, diff, id);
    }
}

static
void diff_insert_removed(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_table_diff_t *diff,
    ecs_id_t id)
{
    diff->removed.array[diff->removed.count ++] = id;

    if (ECS_HAS_RELATION(id, EcsIsA)) {
        /* Removing an IsA relationship also "removes" all components from the
         * instance. Any id from base that's not overridden should be UnSet. */
        diff_insert_removed_isa(world, table, diff, id);
        return;
    }

    if (table->flags & EcsTableHasIsA) {
        if (!ecs_get_typeid(world, id)) {
            /* Do nothing if id is not a component */
            return;
        }

        /* If next table has a base and component is removed, check if
         * the removed component was an override. Removed overrides reexpose the
         * base component, thus "changing" the value which requires an OnSet. */
        if (ecs_search_relation(world, table, 0, id, EcsIsA,
            EcsUp, 0, 0, 0) != -1)
        {
            ids_append(&diff->on_set, id);
            return;
        }
    }

    if (ecs_get_typeid(world, id) != 0) {
        ids_append(&diff->un_set, id);
    }
}

static
void compute_table_diff(
    ecs_world_t *world,
    ecs_table_t *node,
    ecs_table_t *next,
    ecs_graph_edge_t *edge,
    ecs_id_t id)
{
    if (node == next) {
        return;
    }

    if (ECS_HAS_ROLE(id, PAIR)) {
        ecs_id_record_t *idr = flecs_id_record_get(world, ecs_pair(
            ECS_PAIR_FIRST(id), EcsWildcard));
        if (idr->flags & EcsIdUnion) {
            id = ecs_pair(EcsUnion, ECS_PAIR_FIRST(id));
        }
    }

    ecs_type_t node_type = node->type;
    ecs_type_t next_type = next->type;

    ecs_id_t *ids_node = node_type.array;
    ecs_id_t *ids_next = next_type.array;
    int32_t i_node = 0, node_count = node_type.count;
    int32_t i_next = 0, next_count = next_type.count;
    int32_t added_count = 0;
    int32_t removed_count = 0;
    bool trivial_edge = !ECS_HAS_RELATION(id, EcsIsA) && 
        !(node->flags & EcsTableHasIsA) && !(next->flags & EcsTableHasIsA);

    /* First do a scan to see how big the diff is, so we don't have to realloc
     * or alloc more memory than required. */
    for (; i_node < node_count && i_next < next_count; ) {
        ecs_id_t id_node = ids_node[i_node];
        ecs_id_t id_next = ids_next[i_next];

        bool added = id_next < id_node;
        bool removed = id_node < id_next;

        trivial_edge &= !added || id_next == id;
        trivial_edge &= !removed || id_node == id;

        added_count += added;
        removed_count += removed;

        i_node += id_node <= id_next;
        i_next += id_next <= id_node;
    }

    added_count += next_count - i_next;
    removed_count += node_count - i_node;

    trivial_edge &= (added_count + removed_count) <= 1 && 
        !ecs_id_is_wildcard(id);

    if (trivial_edge) {
        /* If edge is trivial there's no need to create a diff element for it.
         * Store whether the id is a tag or not, so that we can still tell 
         * whether an UnSet handler should be called or not. */
        if (node->storage_table != next->storage_table) {
            edge->diff = &ecs_table_edge_is_component;
        }
        return;
    }

    ecs_table_diff_t *diff = ecs_os_calloc_t(ecs_table_diff_t);
    edge->diff = diff;
    if (added_count) {
        diff->added.array = ecs_os_malloc_n(ecs_id_t, added_count);
        diff->added.count = 0;
    }
    if (removed_count) {
        diff->removed.array = ecs_os_malloc_n(ecs_id_t, removed_count);
        diff->removed.count = 0;
    }

    for (i_node = 0, i_next = 0; i_node < node_count && i_next < next_count; ) {
        ecs_id_t id_node = ids_node[i_node];
        ecs_id_t id_next = ids_next[i_next];

        if (id_next < id_node) {
            diff_insert_added(world, node, diff, id_next);
        } else if (id_node < id_next) {
            diff_insert_removed(world, next, diff, id_node);
        }

        i_node += id_node <= id_next;
        i_next += id_next <= id_node;
    }

    for (; i_next < next_count; i_next ++) {
        diff_insert_added(world, node, diff, ids_next[i_next]);
    }
    for (; i_node < node_count; i_node ++) {
        diff_insert_removed(world, next, diff, ids_node[i_node]);
    }

    ecs_assert(diff->added.count == added_count, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(diff->removed.count == removed_count, ECS_INTERNAL_ERROR, NULL);
}

static
void flecs_add_overrides_for_base(
    ecs_world_t *world,
    ecs_type_t *dst_type,
    ecs_id_t pair)
{
    ecs_entity_t base = ecs_pair_second(world, pair);
    ecs_assert(base != 0, ECS_INTERNAL_ERROR, NULL);
    ecs_table_t *base_table = ecs_get_table(world, base);
    if (!base_table) {
        return;
    }
    
    ecs_id_t *ids = base_table->type.array;

    ecs_flags32_t flags = base_table->flags;
    if (flags & EcsTableHasOverrides) {
        int32_t i, count = base_table->type.count;
        for (i = 0; i < count; i ++) {
            ecs_id_t id = ids[i];
            if (ECS_HAS_ROLE(id, OVERRIDE)) {
                flecs_type_add(dst_type, id & ECS_COMPONENT_MASK);
            }
        }
    }

    if (flags & EcsTableHasIsA) {
        const ecs_table_record_t *tr = flecs_id_record_get_table(
            world->idr_isa_wildcard, base_table);
        ecs_assert(tr != NULL, ECS_INTERNAL_ERROR, NULL);
        int32_t i = tr->column, end = i + tr->count;
        for (; i != end; i ++) {
            flecs_add_overrides_for_base(world, dst_type, ids[i]);
        }
    }
}

static
void flecs_add_with_property(
    ecs_world_t *world,
    ecs_id_record_t *idr_with_wildcard,
    ecs_type_t *dst_type,
    ecs_entity_t r,
    ecs_entity_t o)
{
    r = ecs_get_alive(world, r);

    /* Check if component/relationship has With pairs, which contain ids
     * that need to be added to the table. */
    ecs_table_t *table = ecs_get_table(world, r);
    if (!table) {
        return;
    }
    
    const ecs_table_record_t *tr = flecs_id_record_get_table(
        idr_with_wildcard, table);
    if (tr) {
        int32_t i = tr->column, end = i + tr->count;
        ecs_id_t *ids = table->type.array;

        for (; i < end; i ++) {
            ecs_id_t id = ids[i];
            ecs_assert(ECS_PAIR_FIRST(id) == EcsWith, ECS_INTERNAL_ERROR, NULL);
            ecs_id_t ra = ECS_PAIR_SECOND(id);
            ecs_id_t a = ra;
            if (o) {
                a = ecs_pair(ra, o);
            }

            flecs_type_add(dst_type, a);

            flecs_add_with_property(world, idr_with_wildcard, dst_type, ra, o);
        }
    }

}

static
ecs_table_t* flecs_find_table_with(
    ecs_world_t *world,
    ecs_table_t *node,
    ecs_entity_t with)
{    
    ecs_ensure_id(world, with);
    
    ecs_id_record_t *idr = NULL;
    ecs_entity_t r = 0, o = 0;
    if (ECS_HAS_ROLE(with, PAIR)) {
        r = ECS_PAIR_FIRST(with);
        o = ECS_PAIR_SECOND(with);
        idr = flecs_id_record_ensure(world, ecs_pair(r, EcsWildcard));
        if (idr->flags & EcsIdUnion) {
            ecs_type_t dst_type;
            ecs_id_t union_id = ecs_pair(EcsUnion, r);
            int res = flecs_type_new_with(&dst_type, &node->type, union_id);
            if (res == -1) {
                return node;
            }
            return find_or_create(world, &dst_type, true, node);
        } else if (idr->flags & EcsIdExclusive) {
            /* Relationship is exclusive, check if table already has it */
            const ecs_table_record_t *tr = flecs_id_record_get_table(idr, node);
            if (tr) {
                /* Table already has an instance of the relationship, create
                    * a new id sequence with the existing id replaced */
                ecs_type_t dst_type = flecs_type_copy(&node->type);
                dst_type.array[tr->column] = with;
                return find_or_create(world, &dst_type, true, node);
            }
        }
    } else {
        idr = flecs_id_record_ensure(world, with);
        r = with;
    }

    /* Create sequence with new id */
    ecs_type_t dst_type;
    int res = flecs_type_new_with(&dst_type, &node->type, with);
    if (res == -1) {
        return node; /* Current table already has id */
    }

    if (r == EcsIsA) {
        /* If adding a prefab, check if prefab has overrides */
        flecs_add_overrides_for_base(world, &dst_type, with);
    }

    if (idr->flags & EcsIdWith) {
        ecs_id_record_t *idr_with_wildcard = flecs_id_record_get(world,
            ecs_pair(EcsWith, EcsWildcard));
        /* If id has With property, add targets to type */
        flecs_add_with_property(world, idr_with_wildcard, &dst_type, r, o);
    }

    return find_or_create(world, &dst_type, true, node);
}

static
ecs_table_t* flecs_find_table_without(
    ecs_world_t *world,
    ecs_table_t *node,
    ecs_entity_t without)
{
    if (ECS_HAS_ROLE(without, PAIR)) {
        ecs_entity_t r = 0;
        ecs_id_record_t *idr = NULL;
        r = ECS_PAIR_FIRST(without);
        idr = flecs_id_record_get(world, ecs_pair(r, EcsWildcard));
        if (idr && idr->flags & EcsIdUnion) {
            without = ecs_pair(EcsUnion, r);
        }
    }

    /* Create sequence with new id */
    ecs_type_t dst_type;
    int res = flecs_type_new_without(&dst_type, &node->type, without);
    if (res == -1) {
        return node; /* Current table does not have id */
    }

    return find_or_create(world, &dst_type, true, node);
}

static
void init_edge(
    ecs_table_t *table,
    ecs_graph_edge_t *edge,
    ecs_id_t id,
    ecs_table_t *to)
{
    ecs_assert(table != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(edge != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(edge->id == 0, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(edge->hdr.next == NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_assert(edge->hdr.prev == NULL, ECS_INTERNAL_ERROR, NULL);
    
    edge->from = table;
    edge->to = to;
    edge->id = id;
}

static
void flecs_init_edge_for_add(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_graph_edge_t *edge,
    ecs_id_t id,
    ecs_table_t *to)
{
    init_edge(table, edge, id, to);

    ensure_hi_edge(world, &table->node.add, id);

    if (table != to) {
        /* Add edges are appended to refs.next */
        ecs_graph_edge_hdr_t *to_refs = &to->node.refs;
        ecs_graph_edge_hdr_t *next = to_refs->next;
        
        to_refs->next = &edge->hdr;
        edge->hdr.prev = to_refs;

        edge->hdr.next = next;
        if (next) {
            next->prev = &edge->hdr;
        }

        compute_table_diff(world, table, to, edge, id);
    }
}

static
void flecs_init_edge_for_remove(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_graph_edge_t *edge,
    ecs_id_t id,
    ecs_table_t *to)
{
    init_edge(table, edge, id, to);

    ensure_hi_edge(world, &table->node.remove, id);

    if (table != to) {
        /* Remove edges are appended to refs.prev */
        ecs_graph_edge_hdr_t *to_refs = &to->node.refs;
        ecs_graph_edge_hdr_t *prev = to_refs->prev;

        to_refs->prev = &edge->hdr;
        edge->hdr.next = to_refs;

        edge->hdr.prev = prev;
        if (prev) {
            prev->next = &edge->hdr;
        }

        compute_table_diff(world, table, to, edge, id);
    }
}

static
ecs_table_t* flecs_create_edge_for_remove(
    ecs_world_t *world,
    ecs_table_t *node,
    ecs_graph_edge_t *edge,
    ecs_id_t id)
{
    ecs_table_t *to = flecs_find_table_without(world, node, id);
    
    flecs_init_edge_for_remove(world, node, edge, id, to);

    return to;   
}

static
ecs_table_t* flecs_create_edge_for_add(
    ecs_world_t *world,
    ecs_table_t *node,
    ecs_graph_edge_t *edge,
    ecs_id_t id)
{
    ecs_table_t *to = flecs_find_table_with(world, node, id);

    flecs_init_edge_for_add(world, node, edge, id, to);

    return to;
}

static
void populate_diff( 
    ecs_graph_edge_t *edge,
    ecs_id_t *add_ptr,
    ecs_id_t *remove_ptr,
    ecs_table_diff_t *out)
{
    if (out) {
        ecs_table_diff_t *diff = edge->diff;

        if (diff && diff != &ecs_table_edge_is_component) {
            *out = *diff;
        } else {
            out->on_set.count = 0;

            if (add_ptr) {
                out->added.array = add_ptr;
                out->added.count = 1;
            } else {
                out->added.count = 0;
            }

            if (remove_ptr) {
                out->removed.array = remove_ptr;
                out->removed.count = 1;
                if (diff == &ecs_table_edge_is_component) {
                    out->un_set.array = remove_ptr;
                    out->un_set.count = 1;
                } else {
                    out->un_set.count = 0;
                }
            } else {
                out->removed.count = 0;
                out->un_set.count = 0;
            }
        }
    }
}

ecs_table_t* flecs_table_traverse_remove(
    ecs_world_t *world,
    ecs_table_t *node,
    ecs_id_t *id_ptr,
    ecs_table_diff_t *diff)
{
    ecs_poly_assert(world, ecs_world_t);

    node = node ? node : &world->store.root;

    /* Removing 0 from an entity is not valid */
    ecs_check(id_ptr != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(id_ptr[0] != 0, ECS_INVALID_PARAMETER, NULL);

    ecs_id_t id = id_ptr[0];
    ecs_graph_edge_t *edge = ensure_edge(world, &node->node.remove, id);
    ecs_table_t *to = edge->to;

    if (!to) {
        to = flecs_create_edge_for_remove(world, node, edge, id);
        ecs_assert(to != NULL, ECS_INTERNAL_ERROR, NULL);
        ecs_assert(edge->to != NULL, ECS_INTERNAL_ERROR, NULL);
    }

    populate_diff(edge, NULL, id_ptr, diff);

    return to;
error:
    return NULL;
}

ecs_table_t* flecs_table_traverse_add(
    ecs_world_t *world,
    ecs_table_t *node,
    ecs_id_t *id_ptr,
    ecs_table_diff_t *diff)
{
    ecs_poly_assert(world, ecs_world_t);

    node = node ? node : &world->store.root;

    /* Adding 0 to an entity is not valid */
    ecs_check(id_ptr != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(id_ptr[0] != 0, ECS_INVALID_PARAMETER, NULL);

    ecs_id_t id = id_ptr[0];
    ecs_graph_edge_t *edge = ensure_edge(world, &node->node.add, id);
    ecs_table_t *to = edge->to;

    if (!to) {
        to = flecs_create_edge_for_add(world, node, edge, id);
        ecs_assert(to != NULL, ECS_INTERNAL_ERROR, NULL);
        ecs_assert(edge->to != NULL, ECS_INTERNAL_ERROR, NULL);
    }

    populate_diff(edge, id_ptr, NULL, diff);

    return to;
error:
    return NULL;
}

ecs_table_t* flecs_table_find_or_create(
    ecs_world_t *world,
    ecs_type_t *type)
{
    ecs_poly_assert(world, ecs_world_t);
    return find_or_create(world, type, false, NULL);
}

void flecs_init_root_table(
    ecs_world_t *world)
{
    ecs_poly_assert(world, ecs_world_t);

    world->store.root.type = (ecs_type_t){0};

    init_table(world, &world->store.root, NULL);

    /* Ensure table indices start at 1, as 0 is reserved for the root */
    uint64_t new_id = flecs_sparse_new_id(&world->store.tables);
    ecs_assert(new_id == 0, ECS_INTERNAL_ERROR, NULL);
    (void)new_id;
}

void flecs_table_clear_edges(
    ecs_world_t *world,
    ecs_table_t *table)
{
    (void)world;
    ecs_poly_assert(world, ecs_world_t);

    ecs_log_push_1();

    ecs_map_iter_t it;
    ecs_graph_node_t *table_node = &table->node;
    ecs_graph_edges_t *node_add = &table_node->add;
    ecs_graph_edges_t *node_remove = &table_node->remove;
    ecs_map_t *add_hi = &node_add->hi;
    ecs_map_t *remove_hi = &node_remove->hi;
    ecs_graph_edge_hdr_t *node_refs = &table_node->refs;
    ecs_graph_edge_t *edge;
    uint64_t key;

    /* Cleanup outgoing edges */
    it = ecs_map_iter(add_hi);
    while ((edge = ecs_map_next_ptr(&it, ecs_graph_edge_t*, &key))) {
        disconnect_edge(world, key, edge);
    }

    it = ecs_map_iter(remove_hi);
    while ((edge = ecs_map_next_ptr(&it, ecs_graph_edge_t*, &key))) {
        disconnect_edge(world, key, edge);
    }

    /* Cleanup incoming add edges */
    ecs_graph_edge_hdr_t *next, *cur = node_refs->next;
    if (cur) {
        do {
            edge = (ecs_graph_edge_t*)cur;
            ecs_assert(edge->to == table, ECS_INTERNAL_ERROR, NULL);
            ecs_assert(edge->from != NULL, ECS_INTERNAL_ERROR, NULL);
            next = cur->next;
            remove_edge(world, &edge->from->node.add, edge->id, edge);
        } while ((cur = next));
    }

    /* Cleanup incoming remove edges */
    cur = node_refs->prev;
    if (cur) {
        do {
            edge = (ecs_graph_edge_t*)cur;
            ecs_assert(edge->to == table, ECS_INTERNAL_ERROR, NULL);
            ecs_assert(edge->from != NULL, ECS_INTERNAL_ERROR, NULL);
            next = cur->prev;
            remove_edge(world, &edge->from->node.remove, edge->id, edge);
        } while ((cur = next));
    }

    ecs_os_free(node_add->lo);
    ecs_os_free(node_remove->lo);
    ecs_map_fini(add_hi);
    ecs_map_fini(remove_hi);
    table_node->add.lo = NULL;
    table_node->remove.lo = NULL;

    ecs_log_pop_1();
}

/* Public convenience functions for traversing table graph */
ecs_table_t* ecs_table_add_id(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_id_t id)
{
    return flecs_table_traverse_add(world, table, &id, NULL);
}

ecs_table_t* ecs_table_remove_id(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_id_t id)
{
    return flecs_table_traverse_remove(world, table, &id, NULL);
}

#include <stddef.h>

/* Utility macros to enforce consistency when initializing iterator fields */

/* If term count is smaller than cache size, initialize with inline array,
 * otherwise allocate. */
#define INIT_CACHE(it, fields, f, count, cache_size)\
    if (!it->f && (fields & flecs_iter_cache_##f) && count) {\
        if (count <= cache_size) {\
            it->f = it->priv.cache.f;\
            it->priv.cache.used |= flecs_iter_cache_##f;\
        } else {\
            it->f = ecs_os_calloc(ECS_SIZEOF(*(it->f)) * count);\
            it->priv.cache.allocated |= flecs_iter_cache_##f;\
        }\
    }

/* If array is using the cache, make sure that its address is correct in case
 * the iterator got moved (typically happens when returned by a function) */
#define VALIDATE_CACHE(it, f)\
    if (it->f) {\
        if (it->priv.cache.used & flecs_iter_cache_##f) {\
            it->f = it->priv.cache.f;\
        }\
    }

/* If array is allocated, free it when finalizing the iterator */
#define FINI_CACHE(it, f)\
    if (it->f) {\
        if (it->priv.cache.allocated & flecs_iter_cache_##f) {\
            ecs_os_free((void*)it->f);\
        }\
    }

void flecs_iter_init(
    ecs_iter_t *it,
    ecs_flags8_t fields)
{
    ecs_assert(!ECS_BIT_IS_SET(it->flags, EcsIterIsValid), 
        ECS_INTERNAL_ERROR, NULL);

    it->priv.cache.used = 0;
    it->priv.cache.allocated = 0;

    INIT_CACHE(it, fields, ids, it->term_count, ECS_TERM_CACHE_SIZE);
    INIT_CACHE(it, fields, sources, it->term_count, ECS_TERM_CACHE_SIZE);
    INIT_CACHE(it, fields, match_indices, it->term_count, ECS_TERM_CACHE_SIZE);
    INIT_CACHE(it, fields, columns, it->term_count, ECS_TERM_CACHE_SIZE);
    INIT_CACHE(it, fields, variables, it->variable_count, 
        ECS_VARIABLE_CACHE_SIZE);
    INIT_CACHE(it, fields, sizes, it->term_count, ECS_TERM_CACHE_SIZE);

    if (!ECS_BIT_IS_SET(it->flags, EcsIterIsFilter)) {
        INIT_CACHE(it, fields, ptrs, it->term_count, ECS_TERM_CACHE_SIZE);
    } else {
        it->ptrs = NULL;
    }
}

static
void iter_validate_cache(
    ecs_iter_t *it)
{
    /* Make sure pointers to cache are up to date in case iter has moved */
    VALIDATE_CACHE(it, ids);
    VALIDATE_CACHE(it, sources);
    VALIDATE_CACHE(it, match_indices);
    VALIDATE_CACHE(it, columns);
    VALIDATE_CACHE(it, variables);
    VALIDATE_CACHE(it, sizes);
    VALIDATE_CACHE(it, ptrs);
}

void flecs_iter_validate(
    ecs_iter_t *it)
{
    iter_validate_cache(it);
    ECS_BIT_SET(it->flags, EcsIterIsValid);
}

void ecs_iter_fini(
    ecs_iter_t *it)
{
    ECS_BIT_CLEAR(it->flags, EcsIterIsValid);

    if (it->fini) {
        it->fini(it);
    }

    FINI_CACHE(it, ids);
    FINI_CACHE(it, columns);
    FINI_CACHE(it, sources);
    FINI_CACHE(it, sizes);
    FINI_CACHE(it, ptrs);
    FINI_CACHE(it, match_indices);
    FINI_CACHE(it, variables);
}

static
ecs_size_t iter_get_size_for_id(
    ecs_world_t *world,
    ecs_id_t id)
{
    ecs_id_record_t *idr = flecs_id_record_get(world, id);
    if (!idr) {
        return 0;
    }

    if (idr->flags & EcsIdUnion) {
        return ECS_SIZEOF(ecs_entity_t);
    }

    if (idr->type_info) {
        return idr->type_info->size;
    }

    return 0;
}

static
bool flecs_iter_populate_term_data(
    ecs_world_t *world,
    ecs_iter_t *it,
    int32_t t,
    int32_t column,
    void **ptr_out,
    ecs_size_t *size_out)
{
    bool is_shared = false;
    ecs_table_t *table;
    void *data;
    ecs_size_t size = 0;
    int32_t row, u_index;

    if (!column) {
        /* Term has no data. This includes terms that have Not operators. */
        goto no_data;
    }

    if (!it->terms) {
        goto no_data;
    }

    /* Filter terms may match with data but don't return it */
    if (it->terms[t].inout == EcsInOutNone) {
        if (size_out) {
            size = iter_get_size_for_id(world, it->ids[t]);
        }
        goto no_data;
    }

    if (column < 0) {
        is_shared = true;

        /* Data is not from This */
        if (it->references) {
            /* The reference array is used only for components matched on a
             * table (vs. individual entities). Remaining components should be
             * assigned outside of this function */
            if (ecs_term_match_this(&it->terms[t])) {

                /* Iterator provides cached references for non-This terms */
                ecs_ref_t *ref = &it->references[-column - 1];
                if (ptr_out) {
                    if (ref->id) {
                        ptr_out[0] = (void*)ecs_ref_get_id(world, ref, ref->id);
                    } else {
                        ptr_out[0] = NULL;
                    }
                }

                if (!ref->id) {
                    is_shared = false;
                }

                /* If cached references were provided, the code that populated
                * the iterator also had a chance to cache sizes, so size array
                * should already have been assigned. This saves us from having
                * to do additional lookups to find the component size. */
                ecs_assert(size_out == NULL, ECS_INTERNAL_ERROR, NULL);
                return is_shared;
            }

            return true;
        } else {
            ecs_entity_t subj = it->sources[t];
            ecs_assert(subj != 0, ECS_INTERNAL_ERROR, NULL);

            /* Don't use ecs_get_id directly. Instead, go directly to the
             * storage so that we can get both the pointer and size */
            ecs_record_t *r = flecs_entities_get(world, subj);
            ecs_assert(r != NULL && r->table != NULL, ECS_INTERNAL_ERROR, NULL);

            row = ECS_RECORD_TO_ROW(r->row);
            table = r->table;

            ecs_id_t id = it->ids[t];
            ecs_table_t *s_table = table->storage_table;
            ecs_table_record_t *tr;

            if (!s_table || !(tr = flecs_table_record_get(world, s_table, id))){
                u_index = flecs_table_column_to_union_index(table, -column - 1);
                if (u_index != -1) {
                    goto has_union;
                }
                goto no_data;
            }

            /* We now have row and column, so we can get the storage for the id
             * which gives us the pointer and size */
            column = tr->column;
            ecs_type_info_t *ti = table->type_info[column];
            ecs_column_t *s = &table->data.columns[column];
            size = ti->size;
            data = ecs_storage_first(s);
            /* Fallthrough to has_data */
        }
    } else {
        /* Data is from This, use table from iterator */
        table = it->table;
        if (!table) {
            goto no_data;
        }

        row = it->offset;

        int32_t storage_column = ecs_table_type_to_storage_index(
            table, column - 1);
        if (storage_column == -1) {
            u_index = flecs_table_column_to_union_index(table, column - 1);
            if (u_index != -1) {
                goto has_union;
            }
            goto no_data;
        }

        ecs_type_info_t *ti = table->type_info[storage_column];
        ecs_column_t *s = &table->data.columns[storage_column];
        size = ti->size;
        data = ecs_storage_first(s);

        if (!table || !ecs_table_count(table)) {
            goto no_data;
        }

        /* Fallthrough to has_data */
    }

has_data:
    if (ptr_out) ptr_out[0] = ECS_ELEM(data, size, row);
    if (size_out) size_out[0] = size;
    return is_shared;

has_union: {
        /* Edge case: if column is a switch we should return the vector with case
         * identifiers. Will be replaced in the future with pluggable storage */
        ecs_switch_t *sw = &table->data.sw_columns[u_index];
        data = ecs_vector_first(flecs_switch_values(sw), ecs_entity_t);
        size = ECS_SIZEOF(ecs_entity_t);
        goto has_data;
    }

no_data:
    if (ptr_out) ptr_out[0] = NULL;
    if (size_out) size_out[0] = size;
    return false;
}

void flecs_iter_populate_data(
    ecs_world_t *world,
    ecs_iter_t *it,
    ecs_table_t *table,
    int32_t offset,
    int32_t count,
    void **ptrs,
    ecs_size_t *sizes)
{
    if (it->table) {
        it->frame_offset += ecs_table_count(it->table);
    }

    it->table = table;
    it->offset = offset;
    it->count = count;

    if (table) {
        if (!count) {
            count = it->count = ecs_table_count(table);
        }
        if (count) {
            it->entities = ecs_storage_get_t(
                &table->data.entities, ecs_entity_t, offset);
        } else {
            it->entities = NULL;
        }
    }

    int t, term_count = it->term_count;

    if (ECS_BIT_IS_SET(it->flags, EcsIterIsFilter)) {
        ECS_BIT_CLEAR(it->flags, EcsIterHasShared);

        if (!sizes) {
            return;
        }

        /* Fetch sizes, skip fetching data */
        for (t = 0; t < term_count; t ++) {
            sizes[t] = iter_get_size_for_id(world, it->ids[t]);
        }
        return;
    }

    bool has_shared = false;

    if (ptrs && sizes) {
        for (t = 0; t < term_count; t ++) {
            int32_t column = it->columns[t];
            has_shared |= flecs_iter_populate_term_data(world, it, t, column,
                &ptrs[t], 
                &sizes[t]);
        }
    } else {
        for (t = 0; t < term_count; t ++) {
            ecs_assert(it->columns != NULL, ECS_INTERNAL_ERROR, NULL);

            int32_t column = it->columns[t];
            void **ptr = NULL;
            if (ptrs) {
                ptr = &ptrs[t];
            }
            ecs_size_t *size = NULL;
            if (sizes) {
                size = &sizes[t];
            }

            has_shared |= flecs_iter_populate_term_data(world, it, t, column,
                ptr, size);
        }
    }

    ECS_BIT_COND(it->flags, EcsIterHasShared, has_shared);
}

bool flecs_iter_next_row(
    ecs_iter_t *it)
{
    ecs_assert(it != NULL, ECS_INTERNAL_ERROR, NULL);

    bool is_instanced = ECS_BIT_IS_SET(it->flags, EcsIterIsInstanced);
    if (!is_instanced) {
        int32_t instance_count = it->instance_count;
        int32_t count = it->count;
        int32_t offset = it->offset;

        if (instance_count > count && offset < (instance_count - 1)) {
            ecs_assert(count == 1, ECS_INTERNAL_ERROR, NULL);
            int t, term_count = it->term_count;

            for (t = 0; t < term_count; t ++) {
                int32_t column = it->columns[t];
                if (column >= 0) {
                    void *ptr = it->ptrs[t];
                    if (ptr) {
                        it->ptrs[t] = ECS_OFFSET(ptr, it->sizes[t]);
                    }
                }
            }

            if (it->entities) {
                it->entities ++;
            }
            it->offset ++;

            return true;
        }
    }

    return false;
}

bool flecs_iter_next_instanced(
    ecs_iter_t *it,
    bool result)
{
    it->instance_count = it->count;
    bool is_instanced = ECS_BIT_IS_SET(it->flags, EcsIterIsInstanced);
    bool has_shared = ECS_BIT_IS_SET(it->flags, EcsIterHasShared);
    if (result && !is_instanced && it->count && has_shared) {
        it->count = 1;
    }
    return result;
}

/* --- Public API --- */

void* ecs_field_w_size(
    const ecs_iter_t *it,
    size_t size,
    int32_t term)
{
    ecs_check(it->flags & EcsIterIsValid, ECS_INVALID_PARAMETER, NULL);
    ecs_check(!size || ecs_field_size(it, term) == size || 
        (!ecs_field_size(it, term) && (!it->ptrs || !it->ptrs[term - 1])), 
        ECS_INVALID_PARAMETER, NULL);

    (void)size;

    if (!term) {
        return it->entities;
    }

    if (!it->ptrs) {
        return NULL;
    }

    return it->ptrs[term - 1];
error:
    return NULL;
}

bool ecs_field_is_readonly(
    const ecs_iter_t *it,
    int32_t term_index)
{
    ecs_check(it->flags & EcsIterIsValid, ECS_INVALID_PARAMETER, NULL);
    ecs_check(term_index > 0, ECS_INVALID_PARAMETER, NULL);

    ecs_term_t *term = &it->terms[term_index - 1];
    ecs_check(term != NULL, ECS_INVALID_PARAMETER, NULL);
    
    if (term->inout == EcsIn) {
        return true;
    } else {
        ecs_term_id_t *src = &term->src;

        if (term->inout == EcsInOutDefault) {
            if (!(ecs_term_match_this(term))) {
                return true;
            }

            if (!(src->flags & EcsSelf)) {
                return true;
            }
        }
    }

error:
    return false;
}

bool ecs_field_is_writeonly(
    const ecs_iter_t *it,
    int32_t term_index)
{
    ecs_check(it->flags & EcsIterIsValid, ECS_INVALID_PARAMETER, NULL);
    ecs_check(term_index > 0, ECS_INVALID_PARAMETER, NULL);

    ecs_term_t *term = &it->terms[term_index - 1];
    ecs_check(term != NULL, ECS_INVALID_PARAMETER, NULL);
    
    if (term->inout == EcsOut) {
        return true;
    }

error:
    return false;
}

int32_t ecs_iter_find_column(
    const ecs_iter_t *it,
    ecs_entity_t component)
{
    ecs_check(it->flags & EcsIterIsValid, ECS_INVALID_PARAMETER, NULL);
    ecs_check(it->table != NULL, ECS_INVALID_PARAMETER, NULL);
    return ecs_search(it->real_world, it->table, component, 0);
error:
    return -1;
}

bool ecs_field_is_set(
    const ecs_iter_t *it,
    int32_t index)
{
    ecs_check(it->flags & EcsIterIsValid, ECS_INVALID_PARAMETER, NULL);

    int32_t column = it->columns[index - 1];
    if (!column) {
        return false;
    } else if (column < 0) {
        if (it->references) {
            column = -column - 1;
            ecs_ref_t *ref = &it->references[column];
            return ref->entity != 0;
        } else {
            return true;
        }
    }

    return true;
error:
    return false;
}

bool ecs_field_is_self(
    const ecs_iter_t *it,
    int32_t index)
{
    ecs_assert(it != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(index >= 0, ECS_INVALID_PARAMETER, NULL);
    return it->sources == NULL || it->sources[index - 1] == 0;
}

ecs_id_t ecs_field_id(
    const ecs_iter_t *it,
    int32_t index)
{
    ecs_assert(it != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(index >= 0, ECS_INVALID_PARAMETER, NULL);
    return it->ids[index - 1];
}

ecs_entity_t ecs_field_src(
    const ecs_iter_t *it,
    int32_t index)
{
    ecs_assert(it != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(index >= 0, ECS_INVALID_PARAMETER, NULL);
    if (it->sources) {
        return it->sources[index - 1];
    } else {
        return 0;
    }
}

size_t ecs_field_size(
    const ecs_iter_t *it,
    int32_t index)
{
    ecs_assert(it != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_assert(index >= 0, ECS_INVALID_PARAMETER, NULL);

    if (index == 0) {
        return sizeof(ecs_entity_t);
    } else {
        return (size_t)it->sizes[index - 1];
    }
}

void* ecs_iter_column_w_size(
    const ecs_iter_t *it,
    size_t size,
    int32_t index)
{
    ecs_check(it->flags & EcsIterIsValid, ECS_INVALID_PARAMETER, NULL);
    ecs_check(it->table != NULL, ECS_INVALID_PARAMETER, NULL);
    (void)size;
    
    ecs_table_t *table = it->table;
    int32_t storage_index = ecs_table_type_to_storage_index(table, index);
    if (storage_index == -1) {
        return NULL;
    }

    ecs_type_info_t *ti = table->type_info[storage_index];
    ecs_check(!size || (ecs_size_t)size == ti->size, 
        ECS_INVALID_PARAMETER, NULL);
    (void)ti;

    ecs_column_t *column = &table->data.columns[storage_index];
    return ecs_storage_get(column, flecs_uto(int32_t, size), it->offset);
error:
    return NULL;
}

size_t ecs_iter_column_size(
    const ecs_iter_t *it,
    int32_t index)
{
    ecs_check(it->flags & EcsIterIsValid, ECS_INVALID_PARAMETER, NULL);
    ecs_check(it->table != NULL, ECS_INVALID_PARAMETER, NULL);
    
    ecs_table_t *table = it->table;
    int32_t storage_index = ecs_table_type_to_storage_index(table, index);
    if (storage_index == -1) {
        return 0;
    }

    ecs_type_info_t *ti = table->type_info[storage_index];
    return flecs_ito(size_t, ti->size);
error:
    return 0;
}

char* ecs_iter_str(
    const ecs_iter_t *it)
{
    ecs_world_t *world = it->world;
    ecs_strbuf_t buf = ECS_STRBUF_INIT;
    int i;

    if (it->term_count) {
        ecs_strbuf_list_push(&buf, "term: ", ",");
        for (i = 0; i < it->term_count; i ++) {
            ecs_id_t id = ecs_field_id(it, i + 1);
            char *str = ecs_id_str(world, id);
            ecs_strbuf_list_appendstr(&buf, str);
            ecs_os_free(str);
        }
        ecs_strbuf_list_pop(&buf, "\n");

        ecs_strbuf_list_push(&buf, "subj: ", ",");
        for (i = 0; i < it->term_count; i ++) {
            ecs_entity_t subj = ecs_field_src(it, i + 1);
            char *str = ecs_get_fullpath(world, subj);
            ecs_strbuf_list_appendstr(&buf, str);
            ecs_os_free(str);
        }
        ecs_strbuf_list_pop(&buf, "\n");
    }

    if (it->variable_count) {
        int32_t actual_count = 0;
        for (i = 0; i < it->variable_count; i ++) {
            const char *var_name = it->variable_names[i];
            if (!var_name || var_name[0] == '_' || var_name[0] == '.') {
                /* Skip anonymous variables */
                continue;
            }

            ecs_var_t var = it->variables[i];
            if (!var.entity) {
                /* Skip table variables */
                continue;
            }

            if (!actual_count) {
                ecs_strbuf_list_push(&buf, "vars: ", ",");
            }

            char *str = ecs_get_fullpath(world, var.entity);
            ecs_strbuf_list_append(&buf, "%s=%s", var_name, str);
            ecs_os_free(str);

            actual_count ++;
        }
        if (actual_count) {
            ecs_strbuf_list_pop(&buf, "\n");
        }
    }

    if (it->count) {
        ecs_strbuf_appendstr(&buf, "this:\n");
        for (i = 0; i < it->count; i ++) {
            ecs_entity_t e = it->entities[i];
            char *str = ecs_get_fullpath(world, e);
            ecs_strbuf_appendstr(&buf, "    - ");
            ecs_strbuf_appendstr(&buf, str);
            ecs_strbuf_appendstr(&buf, "\n");
            ecs_os_free(str);
        }
    }

    return ecs_strbuf_get(&buf);
}

void ecs_iter_poly(
    const ecs_world_t *world,
    const ecs_poly_t *poly,
    ecs_iter_t *iter_out,
    ecs_term_t *filter)
{
    ecs_iterable_t *iterable = ecs_get_iterable(poly);
    iterable->init(world, poly, iter_out, filter);
}

bool ecs_iter_next(
    ecs_iter_t *iter)
{
    ecs_check(iter != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(iter->next != NULL, ECS_INVALID_PARAMETER, NULL);
    return iter->next(iter);
error:
    return false;
}

int32_t ecs_iter_count(
    ecs_iter_t *it)
{
    ecs_check(it != NULL, ECS_INVALID_PARAMETER, NULL);

    ECS_BIT_SET(it->flags, EcsIterIsFilter);
    ECS_BIT_SET(it->flags, EcsIterIsInstanced);

    int32_t count = 0;
    while (ecs_iter_next(it)) {
        count += it->count;
    }
    return count;
error:
    return 0;
}

bool ecs_iter_is_true(
    ecs_iter_t *it)
{
    ecs_check(it != NULL, ECS_INVALID_PARAMETER, NULL);

    ECS_BIT_SET(it->flags, EcsIterIsFilter);
    ECS_BIT_SET(it->flags, EcsIterIsInstanced);

    bool result = ecs_iter_next(it);
    if (result) {
        ecs_iter_fini(it);
    }
    return result;
error:
    return false;
}

ecs_entity_t ecs_iter_get_var(
    ecs_iter_t *it,
    int32_t var_id)
{
    ecs_check(var_id >= 0, ECS_INVALID_PARAMETER, NULL);
    ecs_check(var_id < it->variable_count, ECS_INVALID_PARAMETER, NULL);
    ecs_check(it->variables != NULL, ECS_INVALID_PARAMETER, NULL);

    ecs_var_t *var = &it->variables[var_id];
    ecs_entity_t e = var->entity;
    if (!e) {
        ecs_table_t *table = var->range.table;
        if (table) {
            if ((var->range.count == 1) || (ecs_table_count(table) == 1)) {
                ecs_assert(ecs_table_count(table) > var->range.offset,
                    ECS_INTERNAL_ERROR, NULL);
                e = ecs_storage_get_t(&table->data.entities, ecs_entity_t,
                    var->range.offset)[0];
            }
        }
    } else {
        ecs_assert(ecs_is_valid(it->real_world, e), ECS_INTERNAL_ERROR, NULL);
    }

    return e;
error:
    return 0;
}

ecs_table_t* ecs_iter_get_var_as_table(
    ecs_iter_t *it,
    int32_t var_id)
{
    ecs_check(var_id >= 0, ECS_INVALID_PARAMETER, NULL);
    ecs_check(var_id < it->variable_count, ECS_INVALID_PARAMETER, NULL);
    ecs_check(it->variables != NULL, ECS_INVALID_PARAMETER, NULL);

    ecs_var_t *var = &it->variables[var_id];
    ecs_table_t *table = var->range.table;
    if (!table) {
        /* If table is not set, try to get table from entity */
        ecs_entity_t e = var->entity;
        if (e) {
            ecs_record_t *r = flecs_entities_get(it->real_world, e);
            if (r) {
                table = r->table;
                if (ecs_table_count(table) != 1) {
                    /* If table contains more than the entity, make sure not to
                     * return a partial table. */
                    return NULL;
                }
            }
        }
    }

    if (table) {
        if (var->range.offset) {
            /* Don't return whole table if only partial table is matched */
            return NULL;
        }

        if (!var->range.count || ecs_table_count(table) == var->range.count) {
            /* Return table if count matches */
            return table;
        }
    }

error:
    return NULL;
}

ecs_table_range_t ecs_iter_get_var_as_range(
    ecs_iter_t *it,
    int32_t var_id)
{
    ecs_check(var_id >= 0, ECS_INVALID_PARAMETER, NULL);
    ecs_check(var_id < it->variable_count, ECS_INVALID_PARAMETER, NULL);
    ecs_check(it->variables != NULL, ECS_INVALID_PARAMETER, NULL);

    ecs_table_range_t result = { 0 };

    ecs_var_t *var = &it->variables[var_id];
    ecs_table_t *table = var->range.table;
    if (!table) {
        ecs_entity_t e = var->entity;
        if (e) {
            ecs_record_t *r = flecs_entities_get(it->real_world, e);
            if (r) {
                result.table = r->table;
                result.offset = ECS_RECORD_TO_ROW(r->row);
                result.count = 1;
            }
        }
    } else {
        result.table = table;
        result.offset = var->range.offset;
        result.count = var->range.count;
        if (!result.count) {
            result.count = ecs_table_count(table);
        }
    }

    return result;
error:
    return (ecs_table_range_t){0};
}

void ecs_iter_set_var(
    ecs_iter_t *it,
    int32_t var_id,
    ecs_entity_t entity)
{
    ecs_check(it != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(var_id >= 0, ECS_INVALID_PARAMETER, NULL);
    ecs_check(var_id < ECS_VARIABLE_COUNT_MAX, ECS_INVALID_PARAMETER, NULL);
    ecs_check(var_id < it->variable_count, ECS_INVALID_PARAMETER, NULL);
    ecs_check(entity != 0, ECS_INVALID_PARAMETER, NULL);
    /* Can't set variable while iterating */
    ecs_check(!(it->flags & EcsIterIsValid), ECS_INVALID_PARAMETER, NULL);
    ecs_check(it->variables != NULL, ECS_INTERNAL_ERROR, NULL);

    iter_validate_cache(it);

    ecs_var_t *var = &it->variables[var_id];
    var->entity = entity;

    ecs_record_t *r = flecs_entities_get(it->real_world, entity);
    if (r) {
        var->range.table = r->table;
        var->range.offset = ECS_RECORD_TO_ROW(r->row);
        var->range.count = 1;
    } else {
        var->range.table = NULL;
        var->range.offset = 0;
        var->range.count = 0;
    }

    it->constrained_vars |= flecs_ito(uint64_t, 1 << var_id);

error:
    return;
}

void ecs_iter_set_var_as_table(
    ecs_iter_t *it,
    int32_t var_id,
    const ecs_table_t *table)
{
    ecs_table_range_t range = { .table = (ecs_table_t*)table };
    ecs_iter_set_var_as_range(it, var_id, &range);
}

void ecs_iter_set_var_as_range(
    ecs_iter_t *it,
    int32_t var_id,
    const ecs_table_range_t *range)
{
    ecs_check(it != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(var_id >= 0, ECS_INVALID_PARAMETER, NULL);
    ecs_check(var_id < ECS_VARIABLE_COUNT_MAX, ECS_INVALID_PARAMETER, NULL);
    ecs_check(var_id < it->variable_count, ECS_INVALID_PARAMETER, NULL);
    ecs_check(range != 0, ECS_INVALID_PARAMETER, NULL);
    ecs_check(range->table != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(!range->offset || range->offset < ecs_table_count(range->table), 
        ECS_INVALID_PARAMETER, NULL);
    ecs_check((range->offset + range->count) <= ecs_table_count(range->table), 
        ECS_INVALID_PARAMETER, NULL);

    /* Can't set variable while iterating */
    ecs_check(!(it->flags & EcsIterIsValid), ECS_INVALID_OPERATION, NULL);

    iter_validate_cache(it);

    ecs_var_t *var = &it->variables[var_id];
    var->range = *range;

    if (range->count == 1) {
        ecs_table_t *table = range->table;
        var->entity = ecs_storage_get_t(
            &table->data.entities, ecs_entity_t, range->offset)[0];
    } else {
        var->entity = 0;
    }

    it->constrained_vars |= flecs_uto(uint64_t, 1 << var_id);

error:
    return;
}

bool ecs_iter_var_is_constrained(
    ecs_iter_t *it,
    int32_t var_id)
{
    return (it->constrained_vars & (flecs_uto(uint64_t, 1 << var_id))) != 0;
}

ecs_iter_t ecs_page_iter(
    const ecs_iter_t *it,
    int32_t offset,
    int32_t limit)
{
    ecs_check(it != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(it->next != NULL, ECS_INVALID_PARAMETER, NULL);

    ecs_iter_t result = *it;
    result.priv.iter.page = (ecs_page_iter_t){
        .offset = offset,
        .limit = limit,
        .remaining = limit
    };
    result.next = ecs_page_next;
    result.chain_it = (ecs_iter_t*)it;

    return result;
error:
    return (ecs_iter_t){ 0 };
}

static
void offset_iter(
    ecs_iter_t *it,
    int32_t offset)
{
    it->entities = &it->entities[offset];

    int32_t t, term_count = it->term_count;
    for (t = 0; t < term_count; t ++) {
        void *ptrs = it->ptrs[t];
        if (!ptrs) {
            continue;
        }

        if (it->sources[t]) {
            continue;
        }

        it->ptrs[t] = ECS_OFFSET(ptrs, offset * it->sizes[t]);
    }
}

static
bool ecs_page_next_instanced(
    ecs_iter_t *it)
{
    ecs_check(it != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(it->chain_it != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(it->next == ecs_page_next, ECS_INVALID_PARAMETER, NULL);

    ecs_iter_t *chain_it = it->chain_it;
    bool instanced = ECS_BIT_IS_SET(it->flags, EcsIterIsInstanced);

    do {
        if (!ecs_iter_next(chain_it)) {
            goto depleted;
        }

        ecs_page_iter_t *iter = &it->priv.iter.page;
        
        /* Copy everything up to the private iterator data */
        ecs_os_memcpy(it, chain_it, offsetof(ecs_iter_t, priv));

        /* Keep instancing setting from original iterator */
        ECS_BIT_COND(it->flags, EcsIterIsInstanced, instanced);

        if (!chain_it->table) {
            goto yield; /* Task query */
        }

        int32_t offset = iter->offset;
        int32_t limit = iter->limit;
        if (!(offset || limit)) {
            if (it->count) {
                goto yield;
            } else {
                goto depleted;
            }
        }

        int32_t count = it->count;
        int32_t remaining = iter->remaining;

        if (offset) {
            if (offset > count) {
                /* No entities to iterate in current table */
                iter->offset -= count;
                it->count = 0;
                continue;
            } else {
                it->offset += offset;
                count = it->count -= offset;
                iter->offset = 0;
                offset_iter(it, offset);
            }
        }

        if (remaining) {
            if (remaining > count) {
                iter->remaining -= count;
            } else {
                it->count = remaining;
                iter->remaining = 0;
            }
        } else if (limit) {
            /* Limit hit: no more entities left to iterate */
            goto done;
        }
    } while (it->count == 0);

yield:
    if (!ECS_BIT_IS_SET(it->flags, EcsIterIsInstanced)) {
        it->offset = 0;
    }

    return true;
done:
    /* Cleanup iterator resources if it wasn't yet depleted */
    ecs_iter_fini(chain_it);
depleted:
error:
    return false;
}

bool ecs_page_next(
    ecs_iter_t *it)
{
    ecs_check(it != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(it->next == ecs_page_next, ECS_INVALID_PARAMETER, NULL);
    ecs_check(it->chain_it != NULL, ECS_INVALID_PARAMETER, NULL);

    ECS_BIT_SET(it->chain_it->flags, EcsIterIsInstanced);

    if (flecs_iter_next_row(it)) {
        return true;
    }

    return flecs_iter_next_instanced(it, ecs_page_next_instanced(it));
error:
    return false;
}

ecs_iter_t ecs_worker_iter(
    const ecs_iter_t *it,
    int32_t index,
    int32_t count)
{
    ecs_check(it != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(it->next != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(count > 0, ECS_INVALID_PARAMETER, NULL);
    ecs_check(index >= 0, ECS_INVALID_PARAMETER, NULL);
    ecs_check(index < count, ECS_INVALID_PARAMETER, NULL);

    return (ecs_iter_t){
        .real_world = it->real_world,
        .world = it->world,
        .priv.iter.worker = {
            .index = index,
            .count = count
        },
        .next = ecs_worker_next,
        .chain_it = (ecs_iter_t*)it,
        .flags = it->flags & EcsIterIsInstanced
    };

error:
    return (ecs_iter_t){ 0 };
}

static
bool ecs_worker_next_instanced(
    ecs_iter_t *it)
{
    ecs_check(it != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(it->chain_it != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(it->next == ecs_worker_next, ECS_INVALID_PARAMETER, NULL);

    bool instanced = ECS_BIT_IS_SET(it->flags, EcsIterIsInstanced);

    ecs_iter_t *chain_it = it->chain_it;
    ecs_worker_iter_t *iter = &it->priv.iter.worker;
    int32_t res_count = iter->count, res_index = iter->index;
    int32_t per_worker, instances_per_worker, first;

    do {
        if (!ecs_iter_next(chain_it)) {
            return false;
        }

        /* Copy everything up to the private iterator data */
        ecs_os_memcpy(it, chain_it, offsetof(ecs_iter_t, priv));

        /* Keep instancing setting from original iterator */
        ECS_BIT_COND(it->flags, EcsIterIsInstanced, instanced);

        int32_t count = it->count;
        int32_t instance_count = it->instance_count;
        per_worker = count / res_count;
        instances_per_worker = instance_count / res_count;
        first = per_worker * res_index;
        count -= per_worker * res_count;

        if (count) {
            if (res_index < count) {
                per_worker ++;
                first += res_index;
            } else {
                first += count;
            }
        }

        if (!per_worker && it->table == NULL) {
            if (res_index == 0) {
                return true;
            } else {
                return false;
            }
        }
    } while (!per_worker);

    it->instance_count = instances_per_worker;
    it->frame_offset += first;

    offset_iter(it, it->offset + first);
    it->count = per_worker;

    if (ECS_BIT_IS_SET(it->flags, EcsIterIsInstanced)) {
        it->offset += first;
    } else {
        it->offset = 0;
    }

    return true;
error:
    return false;
}

bool ecs_worker_next(
    ecs_iter_t *it)
{
    ecs_check(it != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(it->next == ecs_worker_next, ECS_INVALID_PARAMETER, NULL);
    ecs_check(it->chain_it != NULL, ECS_INVALID_PARAMETER, NULL);

    ECS_BIT_SET(it->chain_it->flags, EcsIterIsInstanced);

    if (flecs_iter_next_row(it)) {
        return true;
    }

    return flecs_iter_next_instanced(it, ecs_worker_next_instanced(it));
error:
    return false;
}

#include <time.h>

#ifndef FLECS_NDEBUG
static int64_t s_min[] = { 
    [1] = INT8_MIN, [2] = INT16_MIN, [4] = INT32_MIN, [8] = INT64_MIN };
static int64_t s_max[] = { 
    [1] = INT8_MAX, [2] = INT16_MAX, [4] = INT32_MAX, [8] = INT64_MAX };
static uint64_t u_max[] = { 
    [1] = UINT8_MAX, [2] = UINT16_MAX, [4] = UINT32_MAX, [8] = UINT64_MAX };

uint64_t _flecs_ito(
    size_t size,
    bool is_signed,
    bool lt_zero,
    uint64_t u,
    const char *err)
{
    union {
        uint64_t u;
        int64_t s;
    } v;

    v.u = u;

    if (is_signed) {
        ecs_assert(v.s >= s_min[size], ECS_INVALID_CONVERSION, err);
        ecs_assert(v.s <= s_max[size], ECS_INVALID_CONVERSION, err);
    } else {
        ecs_assert(lt_zero == false, ECS_INVALID_CONVERSION, err);
        ecs_assert(u <= u_max[size], ECS_INVALID_CONVERSION, err);
    }

    return u;
}
#endif

int32_t flecs_next_pow_of_2(
    int32_t n)
{
    n --;
    n |= n >> 1;
    n |= n >> 2;
    n |= n >> 4;
    n |= n >> 8;
    n |= n >> 16;
    n ++;

    return n;
}

/** Convert time to double */
double ecs_time_to_double(
    ecs_time_t t)
{
    double result;
    result = t.sec;
    return result + (double)t.nanosec / (double)1000000000;
}

ecs_time_t ecs_time_sub(
    ecs_time_t t1,
    ecs_time_t t2)
{
    ecs_time_t result;

    if (t1.nanosec >= t2.nanosec) {
        result.nanosec = t1.nanosec - t2.nanosec;
        result.sec = t1.sec - t2.sec;
    } else {
        result.nanosec = t1.nanosec - t2.nanosec + 1000000000;
        result.sec = t1.sec - t2.sec - 1;
    }

    return result;
}

void ecs_sleepf(
    double t)
{
    if (t > 0) {
        int sec = (int)t;
        int nsec = (int)((t - sec) * 1000000000);
        ecs_os_sleep(sec, nsec);
    }
}

double ecs_time_measure(
    ecs_time_t *start)
{
    ecs_time_t stop, temp;
    ecs_os_get_time(&stop);
    temp = stop;
    stop = ecs_time_sub(stop, *start);
    *start = temp;
    return ecs_time_to_double(stop);
}

void* ecs_os_memdup(
    const void *src, 
    ecs_size_t size) 
{
    if (!src) {
        return NULL;
    }
        
    void *dst = ecs_os_malloc(size);
    ecs_assert(dst != NULL, ECS_OUT_OF_MEMORY, NULL);
    ecs_os_memcpy(dst, src, size);  
    return dst;  
}

int flecs_entity_compare(
    ecs_entity_t e1, 
    const void *ptr1, 
    ecs_entity_t e2, 
    const void *ptr2) 
{
    (void)ptr1;
    (void)ptr2;
    return (e1 > e2) - (e1 < e2);
}

int flecs_entity_compare_qsort(
    const void *e1,
    const void *e2)
{
    ecs_entity_t v1 = *(ecs_entity_t*)e1;
    ecs_entity_t v2 = *(ecs_entity_t*)e2;
    return flecs_entity_compare(v1, NULL, v2, NULL);
}

uint64_t flecs_string_hash(
    const void *ptr)
{
    const ecs_hashed_string_t *str = ptr;
    ecs_assert(str->hash != 0, ECS_INTERNAL_ERROR, NULL);
    return str->hash;
}

char* ecs_vasprintf(
    const char *fmt,
    va_list args)
{
    ecs_size_t size = 0;
    char *result  = NULL;
    va_list tmpa;

    va_copy(tmpa, args);

    size = vsnprintf(result, 0, fmt, tmpa);

    va_end(tmpa);

    if ((int32_t)size < 0) { 
        return NULL; 
    }

    result = (char *) ecs_os_malloc(size + 1);

    if (!result) { 
        return NULL; 
    }

    ecs_os_vsprintf(result, fmt, args);

    return result;
}

char* ecs_asprintf(
    const char *fmt,
    ...)
{
    va_list args;
    va_start(args, fmt);
    char *result = ecs_vasprintf(fmt, args);
    va_end(args);
    return result;
}

/*
    This code was taken from sokol_time.h 
    
    zlib/libpng license
    Copyright (c) 2018 Andre Weissflog
    This software is provided 'as-is', without any express or implied warranty.
    In no event will the authors be held liable for any damages arising from the
    use of this software.
    Permission is granted to anyone to use this software for any purpose,
    including commercial applications, and to alter it and redistribute it
    freely, subject to the following restrictions:
        1. The origin of this software must not be misrepresented; you must not
        claim that you wrote the original software. If you use this software in a
        product, an acknowledgment in the product documentation would be
        appreciated but is not required.
        2. Altered source versions must be plainly marked as such, and must not
        be misrepresented as being the original software.
        3. This notice may not be removed or altered from any source
        distribution.
*/


/* -- Identifier Component -- */
static ECS_DTOR(EcsIdentifier, ptr, {
    ecs_os_strset(&ptr->value, NULL);
})

static ECS_COPY(EcsIdentifier, dst, src, {
    ecs_os_strset(&dst->value, src->value);
    dst->hash = src->hash;
    dst->length = src->length;
    dst->index_hash = src->index_hash;
    dst->index = src->index;
})

static ECS_MOVE(EcsIdentifier, dst, src, {
    ecs_os_strset(&dst->value, NULL);
    dst->value = src->value;
    dst->hash = src->hash;
    dst->length = src->length;
    dst->index_hash = src->index_hash;
    dst->index = src->index;

    src->value = NULL;
    src->hash = 0;
    src->index_hash = 0;
    src->index = 0;
    src->length = 0;
})

static
void ecs_on_set(EcsIdentifier)(ecs_iter_t *it) {
    EcsIdentifier *ptr = ecs_field(it, EcsIdentifier, 1);
    
    ecs_world_t *world = it->real_world;
    ecs_entity_t evt = it->event;
    ecs_id_t evt_id = it->event_id;
    ecs_entity_t kind = ECS_PAIR_SECOND(evt_id); /* Name, Symbol, Alias */

    ecs_id_t pair = ecs_childof(0);

    ecs_hashmap_t *name_index = NULL;
    if (kind == EcsSymbol) {
        name_index = &world->symbols;
    } else if (kind == EcsAlias) {
        name_index = &world->aliases;
    } else if (kind == EcsName) {
        ecs_assert(it->table != NULL, ECS_INTERNAL_ERROR, NULL);
        ecs_search(world, it->table, ecs_childof(EcsWildcard), &pair);
        ecs_assert(pair != 0, ECS_INTERNAL_ERROR, NULL);

        if (evt == EcsOnSet) {
            name_index = flecs_id_name_index_ensure(world, pair);
        } else {
            name_index = flecs_id_name_index_get(world, pair);
        }
    }

    int i, count = it->count;
    for (i = 0; i < count; i ++) {
        EcsIdentifier *cur = &ptr[i];
        uint64_t hash;
        ecs_size_t len;
        const char *name = cur->value;

        if (cur->index && cur->index != name_index) {
            /* If index doesn't match up, the value must have been copied from
             * another entity, so reset index & cached index hash */
            cur->index = NULL;
            cur->index_hash = 0;
        }

        if (cur->value && (evt == EcsOnSet)) {
            len = cur->length = ecs_os_strlen(name);
            hash = cur->hash = flecs_hash(name, len);
        } else {
            len = cur->length = 0;
            hash = cur->hash = 0;
            cur->index = NULL;
        }

        if (name_index) {
            uint64_t index_hash = cur->index_hash;
            ecs_entity_t e = it->entities[i];

            if (hash != index_hash) {
                if (index_hash) {
                    flecs_name_index_remove(name_index, e, index_hash);
                }
                if (hash) {
                    flecs_name_index_ensure(name_index, e, name, len, hash);
                    cur->index_hash = hash;
                    cur->index = name_index;
                }
            } else {
                /* Name didn't change, but the string could have been 
                 * reallocated. Make sure name index points to correct string */
                flecs_name_index_update_name(name_index, e, hash, name);
            }
        }
    }
}


/* -- Poly component -- */

static ECS_COPY(EcsPoly, dst, src, {
    (void)dst;
    (void)src;
    ecs_abort(ECS_INVALID_OPERATION, "poly component cannot be copied");
})

static ECS_MOVE(EcsPoly, dst, src, {
    if (dst->poly && (dst->poly != src->poly)) {
        ecs_poly_dtor_t *dtor = ecs_get_dtor(dst->poly);
        ecs_assert(dtor != NULL, ECS_INTERNAL_ERROR, NULL);
        dtor[0](dst->poly);
    }

    dst->poly = src->poly;
    src->poly = NULL;
})

static ECS_DTOR(EcsPoly, ptr, {
    if (ptr->poly) {
        ecs_poly_dtor_t *dtor = ecs_get_dtor(ptr->poly);
        ecs_assert(dtor != NULL, ECS_INTERNAL_ERROR, NULL);
        dtor[0](ptr->poly);
    }
})


/* -- Builtin triggers -- */

static
void assert_relation_unused(
    ecs_world_t *world, 
    ecs_entity_t rel,
    ecs_entity_t property)
{
    if (world->flags & EcsWorldFini) {
        return;
    }

    ecs_vector_t *marked_ids = world->store.marked_ids;
    int32_t i, count = ecs_vector_count(marked_ids);
    for (i = 0; i < count; i ++) {
        ecs_marked_id_t *mid = ecs_vector_get(marked_ids, ecs_marked_id_t, i);
        if (mid->id == ecs_pair(rel, EcsWildcard)) {
            /* If id is being cleaned up, no need to throw error as tables will
             * be cleaned up */
            return;
        }
    }

    if (ecs_id_in_use(world, ecs_pair(rel, EcsWildcard))) {
        char *r_str = ecs_get_fullpath(world, rel);
        char *p_str = ecs_get_fullpath(world, property);

        ecs_throw(ECS_ID_IN_USE, 
            "cannot change property '%s' for relationship '%s': already in use",
            p_str, r_str);
        
        ecs_os_free(r_str);
        ecs_os_free(p_str);
    }

error:
    return;
}

static
bool set_id_flag(
    ecs_id_record_t *idr, 
    ecs_flags32_t flag)
{
    if (!(idr->flags & flag)) {
        idr->flags |= flag;
        return true;
    }
    return false;
}

static
bool unset_id_flag(
    ecs_id_record_t *idr, 
    ecs_flags32_t flag)
{
    if ((idr->flags & flag)) {
        idr->flags &= ~flag;
        return true;
    }
    return false;
}

static
void register_id_flag_for_relation(
    ecs_iter_t *it,
    ecs_entity_t prop,
    ecs_flags32_t flag,
    ecs_flags32_t not_flag,
    ecs_flags32_t entity_flag)
{
    ecs_world_t *world = it->world;
    ecs_entity_t event = it->event;

    int i, count = it->count;
    for (i = 0; i < count; i ++) {
        ecs_entity_t e = it->entities[i];
        bool changed = false;

        if (event == EcsOnAdd) {
            ecs_id_record_t *idr = flecs_id_record_ensure(world, e);
            changed |= set_id_flag(idr, flag);
            idr = flecs_id_record_ensure(world, ecs_pair(e, EcsWildcard));
            do {
                changed |= set_id_flag(idr, flag);
            } while ((idr = idr->first.next));
            if (entity_flag) flecs_add_flag(world, e, entity_flag);
        } else if (event == EcsOnRemove) {
            ecs_id_record_t *idr = flecs_id_record_get(world, e);
            if (idr) changed |= unset_id_flag(idr, not_flag);
            idr = flecs_id_record_get(world, ecs_pair(e, EcsWildcard));
            if (idr) {
                do {
                    changed |= unset_id_flag(idr, not_flag);
                } while ((idr = idr->first.next));
            }
        }

        if (changed) {
            assert_relation_unused(world, e, prop);
        }
    }
}

static
void register_final(ecs_iter_t *it) {
    ecs_world_t *world = it->world;
    
    int i, count = it->count;
    for (i = 0; i < count; i ++) {
        ecs_entity_t e = it->entities[i];
        if (flecs_id_record_get(world, ecs_pair(EcsIsA, e)) != NULL) {
            char *e_str = ecs_get_fullpath(world, e);
            ecs_throw(ECS_ID_IN_USE,
                "cannot change property 'Final' for '%s': already inherited from",
                    e_str);
            ecs_os_free(e_str);
        error:
            continue;
        }
    }
}

static
void register_on_delete(ecs_iter_t *it) {
    ecs_id_t id = ecs_field_id(it, 1);
    register_id_flag_for_relation(it, EcsOnDelete, 
        ECS_ID_ON_DELETE_FLAG(ECS_PAIR_SECOND(id)),
        EcsIdOnDeleteMask,
        EcsEntityObservedId);
}

static
void register_on_delete_object(ecs_iter_t *it) {
    ecs_id_t id = ecs_field_id(it, 1);
    register_id_flag_for_relation(it, EcsOnDeleteTarget, 
        ECS_ID_ON_DELETE_OBJECT_FLAG(ECS_PAIR_SECOND(id)),
        EcsIdOnDeleteObjectMask,
        EcsEntityObservedId);  
}

static
void register_acyclic(ecs_iter_t *it) {
    register_id_flag_for_relation(it, EcsAcyclic, EcsIdAcyclic, 
        EcsIdAcyclic, 0);
}

static
void register_tag(ecs_iter_t *it) {
    register_id_flag_for_relation(it, EcsTag, EcsIdTag, ~EcsIdTag, 0);

    /* Ensure that all id records for tag have type info set to NULL */
    ecs_world_t *world = it->real_world;
    int i, count = it->count;
    for (i = 0; i < count; i ++) {
        ecs_entity_t e = it->entities[i];

        if (it->event == EcsOnAdd) {
            ecs_id_record_t *idr = flecs_id_record_get(world, 
                ecs_pair(e, EcsWildcard));
            ecs_assert(idr != NULL, ECS_INTERNAL_ERROR, NULL);
            do {
                if (idr->type_info != NULL) {
                    assert_relation_unused(world, e, EcsTag);
                }
                idr->type_info = NULL;
            } while ((idr = idr->first.next));
        }
    }
}

static
void register_exclusive(ecs_iter_t *it) {
    register_id_flag_for_relation(it, EcsExclusive, EcsIdExclusive, 
        EcsIdExclusive, 0);
}

static
void register_dont_inherit(ecs_iter_t *it) {
    register_id_flag_for_relation(it, EcsDontInherit, 
        EcsIdDontInherit, EcsIdDontInherit, 0);
}

static
void register_with(ecs_iter_t *it) {
    register_id_flag_for_relation(it, EcsWith, EcsIdWith, 0, 0);
}

static
void register_union(ecs_iter_t *it) {
    register_id_flag_for_relation(it, EcsUnion, EcsIdUnion, 0, 0);
}

static
void on_symmetric_add_remove(ecs_iter_t *it) {
    ecs_entity_t pair = ecs_field_id(it, 1);

    if (!ECS_HAS_ROLE(pair, PAIR)) {
        /* If relationship was not added as a pair, there's nothing to do */
        return;
    }

    ecs_entity_t rel = ECS_PAIR_FIRST(pair);
    ecs_entity_t obj = ECS_PAIR_SECOND(pair);
    ecs_entity_t event = it->event;
    
    int i, count = it->count;
    for (i = 0; i < count; i ++) {
        ecs_entity_t subj = it->entities[i];
        if (event == EcsOnAdd) {
            if (!ecs_has_id(it->real_world, obj, ecs_pair(rel, subj))) {
                ecs_add_pair(it->world, obj, rel, subj);   
            }
        } else {
            if (ecs_has_id(it->real_world, obj, ecs_pair(rel, subj))) {
                ecs_remove_pair(it->world, obj, rel, subj);   
            }
        }
    }
}

static
void register_symmetric(ecs_iter_t *it) {
    ecs_world_t *world = it->real_world;

    int i, count = it->count;
    for (i = 0; i < count; i ++) {
        ecs_entity_t r = it->entities[i];
        assert_relation_unused(world, r, EcsSymmetric);

        /* Create observer that adds the reverse relationship when R(X, Y) is
         * added, or remove the reverse relationship when R(X, Y) is removed. */
        ecs_observer_init(world, &(ecs_observer_desc_t){
            .entity = ecs_entity(world, {.add = {ecs_childof(EcsFlecsInternals)}}),
            .filter.terms[0] = { .id = ecs_pair(r, EcsWildcard) },
            .callback = on_symmetric_add_remove,
            .events = {EcsOnAdd, EcsOnRemove}
        });
    } 
}

static
void on_component(ecs_iter_t *it) {
    ecs_world_t *world = it->world;
    EcsComponent *c = ecs_field(it, EcsComponent, 1);

    int i, count = it->count;
    for (i = 0; i < count; i ++) {
        ecs_entity_t e = it->entities[i];
        if (it->event == EcsOnSet) {
            if (flecs_type_info_init_id(
                world, e, c[i].size, c[i].alignment, NULL))
            {
                assert_relation_unused(world, e, ecs_id(EcsComponent));
            }
        } else if (it->event == EcsOnRemove) {
            flecs_type_info_free(world, e);
        }
    }
}

static
void ensure_module_tag(ecs_iter_t *it) {
    ecs_world_t *world = it->world;

    int i, count = it->count;
    for (i = 0; i < count; i ++) {
        ecs_entity_t e = it->entities[i];
        ecs_entity_t parent = ecs_get_target(world, e, EcsChildOf, 0);
        if (parent) {
            ecs_add_id(world, parent, EcsModule);
        }
    }
}

/* -- Triggers for keeping hashed ids in sync -- */

static
void on_parent_change(ecs_iter_t *it) {
    ecs_world_t *world = it->world;
    ecs_table_t *other_table = it->other_table, *table = it->table;

    int32_t col = ecs_search(it->real_world, table, 
        ecs_pair(ecs_id(EcsIdentifier), EcsName), 0);
    bool has_name = col != -1;
    bool other_has_name = ecs_search(it->real_world, other_table,
        ecs_pair(ecs_id(EcsIdentifier), EcsName), 0) != -1;

    if (!has_name && !other_has_name) {
        /* If tables don't have names, index does not need to be updated */
        return;
    }

    ecs_id_t to_pair = it->event_id;
    ecs_id_t from_pair = ecs_childof(0);

    /* Find the other ChildOf relationship */
    ecs_search(it->real_world, other_table,
        ecs_pair(EcsChildOf, EcsWildcard), &from_pair);

    bool to_has_name = has_name, from_has_name = other_has_name;
    if (it->event == EcsOnRemove) {
        if (from_pair != ecs_childof(0)) {
            /* Because ChildOf is an exclusive relationship, events always come
             * in OnAdd/OnRemove pairs (add for the new, remove for the old 
             * parent). We only need one of those events, so filter out the
             * OnRemove events except for the case where a parent is removed and
             * not replaced with another parent. */
            return;
        }

        ecs_id_t temp = from_pair;
        from_pair = to_pair;
        to_pair = temp;

        to_has_name = other_has_name;
        from_has_name = has_name;
    }

    /* Get the table column with names */
    const EcsIdentifier *names = ecs_iter_column(it, EcsIdentifier, col);

    ecs_hashmap_t *from_index = 0;
    if (from_has_name) {
        from_index = flecs_id_name_index_get(world, from_pair);
    }
    ecs_hashmap_t *to_index = NULL;
    if (to_has_name) {
        to_index = flecs_id_name_index_ensure(world, to_pair);
    }

    int32_t i, count = it->count;
    for (i = 0; i < count; i ++) {
        ecs_entity_t e = it->entities[i];
        const EcsIdentifier *name = &names[i];

        uint64_t index_hash = name->index_hash;
        if (from_index && index_hash) {
            flecs_name_index_remove(from_index, e, index_hash);
        }
        const char *name_str = name->value;
        if (to_index && name_str) {
            ecs_assert(name->hash != 0, ECS_INTERNAL_ERROR, NULL);
            flecs_name_index_ensure(
                to_index, e, name_str, name->length, name->hash);
        }
    }
}


/* -- Iterable mixins -- */

static
void on_event_iterable_init(
    const ecs_world_t *world,
    const ecs_poly_t *poly, /* Observable */
    ecs_iter_t *it,
    ecs_term_t *filter)
{
    ecs_iter_poly(world, poly, it, filter);
    it->event_id = filter->id;
}

/* -- Bootstrapping -- */

#define bootstrap_component(world, table, name)\
    _bootstrap_component(world, table, ecs_id(name), #name, sizeof(name),\
        ECS_ALIGNOF(name))

static
void _bootstrap_component(
    ecs_world_t *world,
    ecs_table_t *table,
    ecs_entity_t entity,
    const char *symbol,
    ecs_size_t size,
    ecs_size_t alignment)
{
    ecs_assert(table != NULL, ECS_INTERNAL_ERROR, NULL);

    ecs_column_t *columns = table->data.columns;
    ecs_assert(columns != NULL, ECS_INTERNAL_ERROR, NULL);

    ecs_record_t *record = flecs_entities_ensure(world, entity);
    record->table = table;

    int32_t index = flecs_table_append(world, table, entity, record, false);
    record->row = ECS_ROW_TO_RECORD(index, 0);

    EcsComponent *component = ecs_storage_first(&columns[0]);
    component[index].size = size;
    component[index].alignment = alignment;

    const char *name = &symbol[3]; /* Strip 'Ecs' */
    ecs_size_t symbol_length = ecs_os_strlen(symbol);
    ecs_size_t name_length = symbol_length - 3;

    EcsIdentifier *name_col = ecs_storage_first(&columns[1]);
    name_col[index].value = ecs_os_strdup(name);
    name_col[index].length = name_length;
    name_col[index].hash = flecs_hash(name, name_length);
    name_col[index].index_hash = 0;
    name_col[index].index = NULL;

    EcsIdentifier *symbol_col = ecs_storage_first(&columns[2]);
    symbol_col[index].value = ecs_os_strdup(symbol);
    symbol_col[index].length = symbol_length;
    symbol_col[index].hash = flecs_hash(symbol, symbol_length);    
    symbol_col[index].index_hash = 0;
    symbol_col[index].index = NULL;
}

/** Initialize component table. This table is manually constructed to bootstrap
 * flecs. After this function has been called, the builtin components can be
 * created. 
 * The reason this table is constructed manually is because it requires the size
 * and alignment of the EcsComponent and EcsIdentifier components, which haven't
 * been created yet */
static
ecs_table_t* bootstrap_component_table(
    ecs_world_t *world)
{
    /* Before creating table, manually set flags for ChildOf/Identifier, as this
     * can no longer be done after they are in use. */
    ecs_id_record_t *idr = flecs_id_record_ensure(world, EcsChildOf);
    idr->flags |= EcsIdOnDeleteObjectDelete | EcsIdDontInherit |
        EcsIdAcyclic | EcsIdTag;
    idr = flecs_id_record_ensure(world, ecs_pair(EcsChildOf, EcsWildcard));
    idr->flags |= EcsIdOnDeleteObjectDelete | EcsIdDontInherit |
        EcsIdAcyclic | EcsIdTag | EcsIdExclusive;

    idr = flecs_id_record_ensure(
        world, ecs_pair(ecs_id(EcsIdentifier), EcsWildcard));
    idr->flags |= EcsIdDontInherit;

    world->idr_childof_0 = flecs_id_record_ensure(world, 
        ecs_pair(EcsChildOf, 0));

    ecs_id_t ids[] = {
        ecs_id(EcsComponent), 
        EcsFinal,
        ecs_pair(ecs_id(EcsIdentifier), EcsName),
        ecs_pair(ecs_id(EcsIdentifier), EcsSymbol),
        ecs_pair(EcsChildOf, EcsFlecsCore),
        ecs_pair(EcsOnDelete, EcsPanic)
    };
    
    ecs_type_t array = {
        .array = ids,
        .count = 6
    };

    ecs_table_t *result = flecs_table_find_or_create(world, &array);
    ecs_data_t *data = &result->data;

    /* Preallocate enough memory for initial components */
    ecs_storage_init_t(&data->entities, ecs_entity_t, EcsFirstUserComponentId);
    ecs_storage_init_t(&data->records, ecs_record_t, EcsFirstUserComponentId);

    ecs_storage_init_t(&data->columns[0], EcsComponent, EcsFirstUserComponentId);
    ecs_storage_init_t(&data->columns[1], EcsIdentifier, EcsFirstUserComponentId);
    ecs_storage_init_t(&data->columns[2], EcsIdentifier, EcsFirstUserComponentId);
    
    return result;
}

static
void bootstrap_entity(
    ecs_world_t *world,
    ecs_entity_t id,
    const char *name,
    ecs_entity_t parent)
{
    char symbol[256];
    ecs_os_strcpy(symbol, "flecs.core.");
    ecs_os_strcat(symbol, name);
    
    ecs_add_pair(world, id, EcsChildOf, parent);
    ecs_set_name(world, id, name);
    ecs_set_symbol(world, id, symbol);

    ecs_assert(ecs_get_name(world, id) != NULL, ECS_INTERNAL_ERROR, NULL);

    if (!parent || parent == EcsFlecsCore) {
        ecs_assert(ecs_lookup_fullpath(world, name) == id, 
            ECS_INTERNAL_ERROR, NULL);
    }
}

void flecs_bootstrap(
    ecs_world_t *world)
{
    ecs_log_push();

    ecs_set_name_prefix(world, "Ecs");

    /* Ensure builtin ids are alive */
    ecs_ensure(world, ecs_id(EcsComponent));
    ecs_ensure(world, EcsFinal);
    ecs_ensure(world, ecs_id(EcsIdentifier));
    ecs_ensure(world, EcsName);
    ecs_ensure(world, EcsSymbol);
    ecs_ensure(world, EcsAlias);
    ecs_ensure(world, EcsChildOf);
    ecs_ensure(world, EcsFlecsCore);
    ecs_ensure(world, EcsOnDelete);
    ecs_ensure(world, EcsPanic);
    ecs_ensure(world, EcsWildcard);
    ecs_ensure(world, EcsAny);
    ecs_ensure(world, EcsTag);

    /* Bootstrap builtin components */
    flecs_type_info_init(world, EcsComponent, { 
        .ctor = ecs_default_ctor,
        .on_set = on_component,
        .on_remove = on_component
    });

    flecs_type_info_init(world, EcsIdentifier, {
        .ctor = ecs_default_ctor,
        .dtor = ecs_dtor(EcsIdentifier),
        .copy = ecs_copy(EcsIdentifier),
        .move = ecs_move(EcsIdentifier),
        .on_set = ecs_on_set(EcsIdentifier),
        .on_remove = ecs_on_set(EcsIdentifier)
    });

    flecs_type_info_init(world, EcsPoly, {
        .ctor = ecs_default_ctor,
        .copy = ecs_copy(EcsPoly),
        .move = ecs_move(EcsPoly),
        .dtor = ecs_dtor(EcsPoly)
    });

    flecs_type_info_init(world, EcsIterable, { 0 });

    /* Cache often used id records on world */
    world->idr_wildcard = flecs_id_record_ensure(world, EcsWildcard);
    world->idr_wildcard_wildcard = flecs_id_record_ensure(world, 
        ecs_pair(EcsWildcard, EcsWildcard));
    world->idr_any = flecs_id_record_ensure(world, EcsAny);

    /* Create table for initial components */
    ecs_table_t *table = bootstrap_component_table(world);
    assert(table != NULL);

    bootstrap_component(world, table, EcsIdentifier);
    bootstrap_component(world, table, EcsComponent);
    bootstrap_component(world, table, EcsIterable);
    bootstrap_component(world, table, EcsPoly);

    world->info.last_component_id = EcsFirstUserComponentId;
    world->info.last_id = EcsFirstUserEntityId;
    world->info.min_id = 0;
    world->info.max_id = 0;

    /* Make EcsOnAdd, EcsOnSet events iterable to enable .yield_existing */
    ecs_set(world, EcsOnAdd, EcsIterable, { .init = on_event_iterable_init });
    ecs_set(world, EcsOnSet, EcsIterable, { .init = on_event_iterable_init });
    
    ecs_observer_init(world, &(ecs_observer_desc_t){
        .entity = ecs_entity(world, {.add = { ecs_childof(EcsFlecsInternals)}}),
        .filter.terms[0] = { .id = EcsTag, .src.flags = EcsSelf },
        .events = {EcsOnAdd, EcsOnRemove},
        .callback = register_tag,
        .yield_existing = true
    });

    /* Populate core module */
    ecs_set_scope(world, EcsFlecsCore);

    flecs_bootstrap_tag(world, EcsName);
    flecs_bootstrap_tag(world, EcsSymbol);
    flecs_bootstrap_tag(world, EcsAlias);

    flecs_bootstrap_tag(world, EcsQuery);
    flecs_bootstrap_tag(world, EcsObserver);

    flecs_bootstrap_tag(world, EcsModule);
    flecs_bootstrap_tag(world, EcsPrivate);
    flecs_bootstrap_tag(world, EcsPrefab);
    flecs_bootstrap_tag(world, EcsDisabled);
    flecs_bootstrap_tag(world, EcsEmpty);

    /* Initialize builtin modules */
    ecs_set_name(world, EcsFlecs, "flecs");
    ecs_add_id(world, EcsFlecs, EcsModule);
    ecs_add_pair(world, EcsFlecs, EcsOnDelete, EcsPanic);

    ecs_add_pair(world, EcsFlecsCore, EcsChildOf, EcsFlecs);
    ecs_set_name(world, EcsFlecsCore, "core");
    ecs_add_id(world, EcsFlecsCore, EcsModule);

    ecs_add_pair(world, EcsFlecsInternals, EcsChildOf, EcsFlecsCore);
    ecs_set_name(world, EcsFlecsInternals, "internals");
    ecs_add_id(world, EcsFlecsInternals, EcsModule);

    /* Initialize builtin entities */
    bootstrap_entity(world, EcsWorld, "World", EcsFlecsCore);
    bootstrap_entity(world, EcsWildcard, "*", EcsFlecsCore);
    bootstrap_entity(world, EcsAny, "_", EcsFlecsCore);
    bootstrap_entity(world, EcsThis, "This", EcsFlecsCore);
    bootstrap_entity(world, EcsVariable, "$", EcsFlecsCore);

    /* Component/relationship properties */
    flecs_bootstrap_tag(world, EcsTransitive);
    flecs_bootstrap_tag(world, EcsReflexive);
    flecs_bootstrap_tag(world, EcsSymmetric);
    flecs_bootstrap_tag(world, EcsFinal);
    flecs_bootstrap_tag(world, EcsDontInherit);
    flecs_bootstrap_tag(world, EcsTag);
    flecs_bootstrap_tag(world, EcsUnion);
    flecs_bootstrap_tag(world, EcsExclusive);
    flecs_bootstrap_tag(world, EcsAcyclic);
    flecs_bootstrap_tag(world, EcsWith);
    flecs_bootstrap_tag(world, EcsOneOf);

    flecs_bootstrap_tag(world, EcsOnDelete);
    flecs_bootstrap_tag(world, EcsOnDeleteTarget);
    flecs_bootstrap_tag(world, EcsRemove);
    flecs_bootstrap_tag(world, EcsDelete);
    flecs_bootstrap_tag(world, EcsPanic);

    flecs_bootstrap_tag(world, EcsDefaultChildComponent);

    /* Builtin relationships */
    flecs_bootstrap_tag(world, EcsIsA);
    flecs_bootstrap_tag(world, EcsChildOf);
    flecs_bootstrap_tag(world, EcsDependsOn);

    /* Builtin events */
    bootstrap_entity(world, EcsOnAdd, "OnAdd", EcsFlecsCore);
    bootstrap_entity(world, EcsOnRemove, "OnRemove", EcsFlecsCore);
    bootstrap_entity(world, EcsOnSet, "OnSet", EcsFlecsCore);
    bootstrap_entity(world, EcsUnSet, "UnSet", EcsFlecsCore);
    bootstrap_entity(world, EcsOnTableEmpty, "OnTableEmpty", EcsFlecsCore);
    bootstrap_entity(world, EcsOnTableFill, "OnTableFilled", EcsFlecsCore);

    /* Tag relationships (relationships that should never have data) */
    ecs_add_id(world, EcsIsA, EcsTag);
    ecs_add_id(world, EcsChildOf, EcsTag);
    ecs_add_id(world, EcsDependsOn, EcsTag);
    ecs_add_id(world, EcsDefaultChildComponent, EcsTag);
    ecs_add_id(world, EcsUnion, EcsTag);

    /* Exclusive properties */
    ecs_add_id(world, EcsChildOf, EcsExclusive);
    ecs_add_id(world, EcsOnDelete, EcsExclusive);
    ecs_add_id(world, EcsOnDeleteTarget, EcsExclusive);
    ecs_add_id(world, EcsDefaultChildComponent, EcsExclusive);
    ecs_add_id(world, EcsOneOf, EcsExclusive);

    /* Sync properties of ChildOf and Identifier with bootstrapped flags */
    ecs_add_pair(world, EcsChildOf, EcsOnDeleteTarget, EcsDelete);
    ecs_add_id(world, EcsChildOf, EcsAcyclic);
    ecs_add_id(world, EcsChildOf, EcsDontInherit);
    ecs_add_id(world, ecs_id(EcsIdentifier), EcsDontInherit);

    /* The (IsA, *) id record is used often in searches, so cache it */
    world->idr_isa_wildcard = flecs_id_record_ensure(world, 
        ecs_pair(EcsIsA, EcsWildcard));

    /* Create triggers in internals scope */
    ecs_set_scope(world, EcsFlecsInternals);

    ecs_observer_init(world, &(ecs_observer_desc_t){
        .filter.terms[0] = { 
            .id = ecs_pair(EcsChildOf, EcsWildcard),
            .src.flags = EcsSelf
        },
        .events = { EcsOnAdd, EcsOnRemove },
        .yield_existing = true,
        .callback = on_parent_change
    });

    ecs_observer_init(world, &(ecs_observer_desc_t){
        .filter.terms[0] = {.id = EcsFinal, .src.flags = EcsSelf },
        .events = {EcsOnAdd},
        .callback = register_final
    });

    ecs_observer_init(world, &(ecs_observer_desc_t){
        .filter.terms[0] = {.id = ecs_pair(EcsOnDelete, EcsWildcard), .src.flags = EcsSelf },
        .events = {EcsOnAdd, EcsOnRemove},
        .callback = register_on_delete
    });

    ecs_observer_init(world, &(ecs_observer_desc_t){
        .filter.terms[0] = {.id = ecs_pair(EcsOnDeleteTarget, EcsWildcard), .src.flags = EcsSelf },
        .events = {EcsOnAdd, EcsOnRemove},
        .callback = register_on_delete_object
    });

    ecs_observer_init(world, &(ecs_observer_desc_t){
        .filter.terms[0] = {.id = EcsAcyclic, .src.flags = EcsSelf },
        .events = {EcsOnAdd, EcsOnRemove},
        .callback = register_acyclic
    });

    ecs_observer_init(world, &(ecs_observer_desc_t){
        .filter.terms[0] = {.id = EcsExclusive, .src.flags = EcsSelf  },
        .events = {EcsOnAdd},
        .callback = register_exclusive
    });

    ecs_observer_init(world, &(ecs_observer_desc_t){
        .filter.terms[0] = {.id = EcsSymmetric, .src.flags = EcsSelf  },
        .events = {EcsOnAdd},
        .callback = register_symmetric
    });

    ecs_observer_init(world, &(ecs_observer_desc_t){
        .filter.terms[0] = {.id = EcsDontInherit, .src.flags = EcsSelf },
        .events = {EcsOnAdd},
        .callback = register_dont_inherit
    });

    ecs_observer_init(world, &(ecs_observer_desc_t){
        .filter.terms[0] = {.id = ecs_pair(EcsWith, EcsWildcard), .src.flags = EcsSelf },
        .events = {EcsOnAdd},
        .callback = register_with
    });

    ecs_observer_init(world, &(ecs_observer_desc_t){
        .filter.terms[0] = {.id = EcsUnion, .src.flags = EcsSelf },
        .events = {EcsOnAdd},
        .callback = register_union
    });

    /* Define observer to make sure that adding a module to a child entity also
     * adds it to the parent. */
    ecs_observer_init(world, &(ecs_observer_desc_t){
        .filter.terms[0] = {.id = EcsModule, .src.flags = EcsSelf },
        .events = {EcsOnAdd},
        .callback = ensure_module_tag
    });

    /* Set scope back to flecs core */
    ecs_set_scope(world, EcsFlecsCore);

    /* Acyclic components */
    ecs_add_id(world, EcsIsA, EcsAcyclic);
    ecs_add_id(world, EcsDependsOn, EcsAcyclic);
    ecs_add_id(world, EcsWith, EcsAcyclic);

    /* DontInherit components */
    ecs_add_id(world, EcsDisabled, EcsDontInherit);
    ecs_add_id(world, EcsPrefab, EcsDontInherit);

    /* Transitive relationships are always Acyclic */
    ecs_add_pair(world, EcsTransitive, EcsWith, EcsAcyclic);

    /* Transitive relationships */
    ecs_add_id(world, EcsIsA, EcsTransitive);
    ecs_add_id(world, EcsIsA, EcsReflexive);

    /* Run bootstrap functions for other parts of the code */
    flecs_bootstrap_hierarchy(world);

    ecs_set_scope(world, 0);

    ecs_log_pop();
}

#include <stdio.h>
#include <ctype.h>

#define ECS_NAME_BUFFER_LENGTH (64)

static
bool path_append(
    const ecs_world_t *world, 
    ecs_entity_t parent, 
    ecs_entity_t child, 
    const char *sep,
    const char *prefix,
    ecs_strbuf_t *buf)
{
    ecs_poly_assert(world, ecs_world_t);

    ecs_entity_t cur = 0;
    char buff[22];
    const char *name;

    if (ecs_is_valid(world, child)) {
        cur = ecs_get_target(world, child, EcsChildOf, 0);
        if (cur) {
            ecs_assert(cur != child, ECS_CYCLE_DETECTED, NULL);
            if (cur != parent && (cur != EcsFlecsCore || prefix != NULL)) {
                path_append(world, parent, cur, sep, prefix, buf);
                ecs_strbuf_appendstr(buf, sep);
            }
        } else if (prefix) {
            ecs_strbuf_appendstr(buf, prefix);
        }

        name = ecs_get_name(world, child);
        if (!name || !ecs_os_strlen(name)) {
            ecs_os_sprintf(buff, "%u", (uint32_t)child);
            name = buff;
        }        
    } else {
        ecs_os_sprintf(buff, "%u", (uint32_t)child);
        name = buff;
    }

    ecs_strbuf_appendstr(buf, name);

    return cur != 0;
}

static
bool is_number(
    const char *name)
{
    ecs_assert(name != NULL, ECS_INTERNAL_ERROR, NULL);
    
    if (!isdigit(name[0])) {
        return false;
    }

    ecs_size_t i, length = ecs_os_strlen(name);
    for (i = 1; i < length; i ++) {
        char ch = name[i];

        if (!isdigit(ch)) {
            break;
        }
    }

    return i >= length;
}

static 
ecs_entity_t name_to_id(
    const ecs_world_t *world,
    const char *name)
{
    long int result = atol(name);
    ecs_assert(result >= 0, ECS_INTERNAL_ERROR, NULL);
    ecs_entity_t alive = ecs_get_alive(world, (ecs_entity_t)result);
    if (alive) {
        return alive;
    } else {
        return (ecs_entity_t)result;
    }
}

static
ecs_entity_t get_builtin(
    const char *name)
{
    if (name[0] == '.' && name[1] == '\0') {
        return EcsThis;
    } else if (name[0] == '*' && name[1] == '\0') {
        return EcsWildcard;
    } else if (name[0] == '_' && name[1] == '\0') {
        return EcsAny;
    }

    return 0;
}

static
bool is_sep(
    const char **ptr,
    const char *sep)
{
    ecs_size_t len = ecs_os_strlen(sep);

    if (!ecs_os_strncmp(*ptr, sep, len)) {
        *ptr += len;
        return true;
    } else {
        return false;
    }
}

static
const char* path_elem(
    const char *path,
    const char *sep,
    int32_t *len)
{
    const char *ptr;
    char ch;
    int32_t template_nesting = 0;
    int32_t count = 0;

    for (ptr = path; (ch = *ptr); ptr ++) {
        if (ch == '<') {
            template_nesting ++;
        } else if (ch == '>') {
            template_nesting --;
        }

        ecs_check(template_nesting >= 0, ECS_INVALID_PARAMETER, path);

        if (!template_nesting && is_sep(&ptr, sep)) {
            break;
        }

        count ++;
    }

    if (len) {
        *len = count;
    }

    if (count) {
        return ptr;
    } else {
        return NULL;
    }
error:
    return NULL;
}

static
ecs_entity_t get_parent_from_path(
    const ecs_world_t *world,
    ecs_entity_t parent,
    const char **path_ptr,
    const char *prefix,
    bool new_entity)
{
    bool start_from_root = false;
    const char *path = *path_ptr;
   
    if (prefix) {
        ecs_size_t len = ecs_os_strlen(prefix);
        if (!ecs_os_strncmp(path, prefix, len)) {
            path += len;
            parent = 0;
            start_from_root = true;
        }
    }

    if (!start_from_root && !parent && new_entity) {
        parent = ecs_get_scope(world);
    }

    *path_ptr = path;

    return parent;
}

static
void on_set_symbol(ecs_iter_t *it) {
    EcsIdentifier *n = ecs_field(it, EcsIdentifier, 1);
    ecs_world_t *world = it->world;

    int i;
    for (i = 0; i < it->count; i ++) {
        ecs_entity_t e = it->entities[i];
        flecs_name_index_ensure(
            &world->symbols, e, n[i].value, n[i].length, n[i].hash);
    }
}

void flecs_bootstrap_hierarchy(ecs_world_t *world) {
    ecs_observer_init(world, &(ecs_observer_desc_t){
        .entity = ecs_entity(world, {.add = {ecs_childof(EcsFlecsInternals)}}),
        .filter.terms[0] = {.id = ecs_pair(ecs_id(EcsIdentifier), EcsSymbol), .src.flags = EcsSelf },
        .callback = on_set_symbol,
        .events = {EcsOnSet},
        .yield_existing = true
    });
}


/* Public functions */

void ecs_get_path_w_sep_buf(
    const ecs_world_t *world,
    ecs_entity_t parent,
    ecs_entity_t child,
    const char *sep,
    const char *prefix,
    ecs_strbuf_t *buf)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_check(buf != NULL, ECS_INVALID_PARAMETER, NULL);

    world = ecs_get_world(world);

    if (child == EcsWildcard) {
        ecs_strbuf_appendstr(buf, "*");
        return;
    }
    if (child == EcsAny) {
        ecs_strbuf_appendstr(buf, "_");
        return;
    }

    if (!sep) {
        sep = ".";
    }

    if (!child || parent != child) {
        path_append(world, parent, child, sep, prefix, buf);
    } else {
        ecs_strbuf_appendstr(buf, "");
    }

error:
    return;
}

char* ecs_get_path_w_sep(
    const ecs_world_t *world,
    ecs_entity_t parent,
    ecs_entity_t child,
    const char *sep,
    const char *prefix)
{
    ecs_strbuf_t buf = ECS_STRBUF_INIT;
    ecs_get_path_w_sep_buf(world, parent, child, sep, prefix, &buf);
    return ecs_strbuf_get(&buf);
}

ecs_entity_t ecs_lookup_child(
    const ecs_world_t *world,
    ecs_entity_t parent,
    const char *name)
{
    ecs_check(world != NULL, ECS_INTERNAL_ERROR, NULL);
    world = ecs_get_world(world);

    if (is_number(name)) {
        return name_to_id(world, name);
    }

    ecs_id_t pair = ecs_childof(parent);
    ecs_hashmap_t *index = flecs_id_name_index_get(world, pair);
    if (index) {
        return flecs_name_index_find(index, name, 0, 0);
    } else {
        return 0;
    }
error:
    return 0;
}

ecs_entity_t ecs_lookup(
    const ecs_world_t *world,
    const char *name)
{   
    if (!name) {
        return 0;
    }

    ecs_check(world != NULL, ECS_INTERNAL_ERROR, NULL);
    world = ecs_get_world(world);

    ecs_entity_t e = get_builtin(name);
    if (e) {
        return e;
    }

    if (is_number(name)) {
        return name_to_id(world, name);
    }

    e = flecs_name_index_find(&world->aliases, name, 0, 0);
    if (e) {
        return e;
    }    
    
    return ecs_lookup_child(world, 0, name);
error:
    return 0;
}

ecs_entity_t ecs_lookup_symbol(
    const ecs_world_t *world,
    const char *name,
    bool lookup_as_path)
{   
    if (!name) {
        return 0;
    }

    ecs_check(world != NULL, ECS_INTERNAL_ERROR, NULL);
    world = ecs_get_world(world);

    ecs_entity_t e = flecs_name_index_find(&world->symbols, name, 0, 0);
    if (e) {
        return e;
    }

    if (lookup_as_path) {
        return ecs_lookup_fullpath(world, name);
    }

error:
    return 0;
}

ecs_entity_t ecs_lookup_path_w_sep(
    const ecs_world_t *world,
    ecs_entity_t parent,
    const char *path,
    const char *sep,
    const char *prefix,
    bool recursive)
{
    if (!path) {
        return 0;
    }

    if (!sep) {
        sep = ".";
    }

    ecs_check(world != NULL, ECS_INTERNAL_ERROR, NULL);
    const ecs_world_t *stage = world;
    world = ecs_get_world(world);

    ecs_entity_t e = get_builtin(path);
    if (e) {
        return e;
    }

    e = flecs_name_index_find(&world->aliases, path, 0, 0);
    if (e) {
        return e;
    }

    char buff[ECS_NAME_BUFFER_LENGTH];
    const char *ptr, *ptr_start;
    char *elem = buff;
    int32_t len, size = ECS_NAME_BUFFER_LENGTH;
    ecs_entity_t cur;
    bool lookup_path_search = false;

    ecs_entity_t *lookup_path = ecs_get_lookup_path(stage);
    ecs_entity_t *lookup_path_cur = lookup_path;
    while (lookup_path_cur && *lookup_path_cur) {
        lookup_path_cur ++;
    }

    if (!sep) {
        sep = ".";
    }

    parent = get_parent_from_path(stage, parent, &path, prefix, true);

retry:
    cur = parent;
    ptr_start = ptr = path;

    while ((ptr = path_elem(ptr, sep, &len))) {
        if (len < size) {
            ecs_os_memcpy(elem, ptr_start, len);
        } else {
            if (size == ECS_NAME_BUFFER_LENGTH) {
                elem = NULL;
            }

            elem = ecs_os_realloc(elem, len + 1);
            ecs_os_memcpy(elem, ptr_start, len);
            size = len + 1;
        }

        elem[len] = '\0';
        ptr_start = ptr;

        cur = ecs_lookup_child(world, cur, elem);
        if (!cur) {
            goto tail;
        }
    }

tail:
    if (!cur && recursive) {
        if (!lookup_path_search) {
            if (parent) {
                parent = ecs_get_target(world, parent, EcsChildOf, 0);
                goto retry;
            } else {
                lookup_path_search = true;
            }
        }

        if (lookup_path_search) {
            if (lookup_path_cur != lookup_path) {
                lookup_path_cur --;
                parent = lookup_path_cur[0];
                goto retry;
            }
        }
    }

    if (elem != buff) {
        ecs_os_free(elem);
    }

    return cur;
error:
    return 0;
}

ecs_entity_t ecs_set_scope(
    ecs_world_t *world,
    ecs_entity_t scope)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_stage_t *stage = flecs_stage_from_world(&world);

    ecs_entity_t cur = stage->scope;
    stage->scope = scope;

    return cur;
error:
    return 0;
}

ecs_entity_t ecs_get_scope(
    const ecs_world_t *world)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    const ecs_stage_t *stage = flecs_stage_from_readonly_world(world);
    return stage->scope;
error:
    return 0;
}

ecs_entity_t* ecs_set_lookup_path(
    ecs_world_t *world,
    const ecs_entity_t *lookup_path)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    ecs_stage_t *stage = flecs_stage_from_world(&world);

    ecs_entity_t *cur = stage->lookup_path;
    stage->lookup_path = (ecs_entity_t*)lookup_path;

    return cur;
error:
    return NULL;
}

ecs_entity_t* ecs_get_lookup_path(
    const ecs_world_t *world)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);
    const ecs_stage_t *stage = flecs_stage_from_readonly_world(world);
    return stage->lookup_path;
error:
    return NULL;
}

const char* ecs_set_name_prefix(
    ecs_world_t *world,
    const char *prefix)
{
    ecs_poly_assert(world, ecs_world_t);
    const char *old_prefix = world->name_prefix;
    world->name_prefix = prefix;
    return old_prefix;
}

ecs_entity_t ecs_add_path_w_sep(
    ecs_world_t *world,
    ecs_entity_t entity,
    ecs_entity_t parent,
    const char *path,
    const char *sep,
    const char *prefix)
{
    ecs_check(world != NULL, ECS_INVALID_PARAMETER, NULL);

    if (!sep) {
        sep = ".";
    }    

    if (!path) {
        if (!entity) {
            entity = ecs_new_id(world);
        }

        if (parent) {
            ecs_add_pair(world, entity, EcsChildOf, entity);
        }

        return entity;
    }

    parent = get_parent_from_path(world, parent, &path, prefix, entity == 0);

    char buff[ECS_NAME_BUFFER_LENGTH];
    const char *ptr = path;
    const char *ptr_start = path;
    char *elem = buff;
    int32_t len, size = ECS_NAME_BUFFER_LENGTH;

    ecs_entity_t cur = parent;

    char *name = NULL;

    while ((ptr = path_elem(ptr, sep, &len))) {
        if (len < size) {
            ecs_os_memcpy(elem, ptr_start, len);
        } else {
            if (size == ECS_NAME_BUFFER_LENGTH) {
                elem = NULL;
            }

            elem = ecs_os_realloc(elem, len + 1);
            ecs_os_memcpy(elem, ptr_start, len);
            size = len + 1;          
        }

        elem[len] = '\0';
        ptr_start = ptr;

        ecs_entity_t e = ecs_lookup_child(world, cur, elem);
        if (!e) {
            if (name) {
                ecs_os_free(name);
            }

            name = ecs_os_strdup(elem);

            /* If this is the last entity in the path, use the provided id */
            bool last_elem = false;
            if (!path_elem(ptr, sep, NULL)) {
                e = entity;
                last_elem = true;
            }

            if (!e) {
                if (last_elem) {
                    ecs_entity_t prev = ecs_set_scope(world, 0);
                    e = ecs_new(world, 0);
                    ecs_set_scope(world, prev);
                } else {
                    e = ecs_new_id(world);
                }
            }

            if (cur) {
                ecs_add_pair(world, e, EcsChildOf, cur);
            }

            ecs_set_name(world, e, name);
        }

        cur = e;
    }

    if (entity && (cur != entity)) {
        ecs_throw(ECS_ALREADY_DEFINED, name);
    }

    if (name) {
        ecs_os_free(name);
    }

    if (elem != buff) {
        ecs_os_free(elem);
    }

    return cur;
error:
    return 0;
}

ecs_entity_t ecs_new_from_path_w_sep(
    ecs_world_t *world,
    ecs_entity_t parent,
    const char *path,
    const char *sep,
    const char *prefix)
{
    if (!sep) {
        sep = ".";
    }

    return ecs_add_path_w_sep(world, 0, parent, path, sep, prefix);
}


static
ecs_id_record_elem_t* id_record_elem(
    ecs_id_record_t *head,
    ecs_id_record_elem_t *list,
    ecs_id_record_t *idr)
{
    return ECS_OFFSET(idr, (uintptr_t)list - (uintptr_t)head);
}

static
void id_record_elem_insert(
    ecs_id_record_t *head,
    ecs_id_record_t *idr,
    ecs_id_record_elem_t *elem)
{
    ecs_id_record_elem_t *head_elem = id_record_elem(idr, elem, head);
    ecs_id_record_t *cur = head_elem->next;
    elem->next = cur;
    elem->prev = head;
    if (cur) {
        ecs_id_record_elem_t *cur_elem = id_record_elem(idr, elem, cur);
        cur_elem->prev = idr;
    }
    head_elem->next = idr;
}

static
void id_record_elem_remove(
    ecs_id_record_t *idr,
    ecs_id_record_elem_t *elem)
{
    ecs_id_record_t *prev = elem->prev;
    ecs_id_record_t *next = elem->next;
    ecs_assert(prev != NULL, ECS_INTERNAL_ERROR, NULL);

    ecs_id_record_elem_t *prev_elem = id_record_elem(idr, elem, prev);
    prev_elem->next = next;
    if (next) {
        ecs_id_record_elem_t *next_elem = id_record_elem(idr, elem, next);
        next_elem->prev = prev;
    }
}

static
void insert_id_elem(
    ecs_world_t *world,
    ecs_id_record_t *idr,
    ecs_id_t wildcard,
    ecs_id_record_t *widr)
{
    ecs_assert(ecs_id_is_wildcard(wildcard), ECS_INTERNAL_ERROR, NULL);
    if (!widr) {
        widr = flecs_id_record_ensure(world, wildcard);
    }
    ecs_assert(widr != NULL, ECS_INTERNAL_ERROR, NULL);

    if (ECS_PAIR_SECOND(wildcard) == EcsWildcard) {
        ecs_assert(ECS_PAIR_FIRST(wildcard) != EcsWildcard, 
            ECS_INTERNAL_ERROR, NULL);
        id_record_elem_insert(widr, idr, &idr->first);
    } else {
        ecs_assert(ECS_PAIR_FIRST(wildcard) == EcsWildcard, 
            ECS_INTERNAL_ERROR, NULL);
        id_record_elem_insert(widr, idr, &idr->second);

        if (idr->flags & EcsIdAcyclic) {
            id_record_elem_insert(widr, idr, &idr->acyclic);
        }
    }
}

static
void remove_id_elem(
    ecs_id_record_t *idr,
    ecs_id_t wildcard)
{
    ecs_assert(ecs_id_is_wildcard(wildcard), ECS_INTERNAL_ERROR, NULL);

    if (ECS_PAIR_SECOND(wildcard) == EcsWildcard) {
        ecs_assert(ECS_PAIR_FIRST(wildcard) != EcsWildcard, 
            ECS_INTERNAL_ERROR, NULL);
        id_record_elem_remove(idr, &idr->first);
    } else {
        ecs_assert(ECS_PAIR_FIRST(wildcard) == EcsWildcard, 
            ECS_INTERNAL_ERROR, NULL);
        id_record_elem_remove(idr, &idr->second);

        if (idr->flags & EcsIdAcyclic) {
            id_record_elem_remove(idr, &idr->acyclic);
        }
    }
}

static
ecs_id_t flecs_id_record_id(
    ecs_id_t id)
{
    id = ecs_strip_generation(id);
    if (ECS_HAS_ROLE(id, PAIR)) {
        ecs_entity_t r = ECS_PAIR_FIRST(id);
        ecs_entity_t o = ECS_PAIR_SECOND(id);
        if (r == EcsAny) {
            r = EcsWildcard;
        }
        if (o == EcsAny) {
            o = EcsWildcard;
        }
        id = ecs_pair(r, o);
    }
    return id;
}

static
ecs_id_record_t* flecs_id_record_new(
    ecs_world_t *world,
    ecs_id_t id)
{
    ecs_id_record_t *idr = ecs_os_calloc_t(ecs_id_record_t);
    ecs_table_cache_init(&idr->cache);

    idr->id = id;
    idr->refcount = 1;

    bool is_wildcard = ecs_id_is_wildcard(id);

    ecs_entity_t rel = 0, obj = 0, role = id & ECS_ROLE_MASK;
    if (role == ECS_PAIR) {
        rel = ecs_pair_first(world, id);
        ecs_assert(rel != 0, ECS_INTERNAL_ERROR, NULL);

        /* Relationship object can be 0, as tables without a ChildOf relationship are
         * added to the (ChildOf, 0) id record */
        obj = ECS_PAIR_SECOND(id);
        if (obj) {
            obj = ecs_get_alive(world, obj);
            ecs_assert(obj != 0, ECS_INTERNAL_ERROR, NULL);
        }

        /* Check constraints */
        if (obj && !ecs_id_is_wildcard(obj)) {
            ecs_entity_t oneof = flecs_get_oneof(world, rel);
            ecs_check( !oneof || ecs_has_pair(world, obj, EcsChildOf, oneof),
                ECS_CONSTRAINT_VIOLATED, NULL);
            (void)oneof;
        }

        if (!is_wildcard) {
            /* Inherit flags from (relationship, *) record */
            ecs_id_record_t *idr_r = flecs_id_record_ensure(
                world, ecs_pair(rel, EcsWildcard));
            idr->parent = idr_r;
            idr->flags = idr_r->flags;

            /* If pair is not a wildcard, append it to wildcard lists. These 
             * allow for quickly enumerating all relationships for an object, or all 
             * objecs for a relationship. */
            insert_id_elem(world, idr, ecs_pair(rel, EcsWildcard), idr_r);
            insert_id_elem(world, idr, ecs_pair(EcsWildcard, obj), NULL);

            if (rel == EcsUnion) {
                idr->flags |= EcsIdUnion;
            }
        }
    } else {
        rel = id & ECS_COMPONENT_MASK;
        rel = ecs_get_alive(world, rel);
        ecs_assert(rel != 0, ECS_INTERNAL_ERROR, NULL);
    }

    /* Initialize type info if id is not a tag */
    if (!is_wildcard && (!role || (role == ECS_PAIR))) {
        if (!(idr->flags & EcsIdTag)) {
            const ecs_type_info_t *ti = flecs_type_info_get(world, rel);
            if (!ti && obj) {
                ti = flecs_type_info_get(world, obj);
            }
            idr->type_info = ti;
        }
    }

    /* Mark entities that are used as component/pair ids. When a tracked
     * entity is deleted, cleanup policies are applied so that the store
     * won't contain any tables with deleted ids. */

    /* Flag for OnDelete policies */
    flecs_add_flag(world, rel, EcsEntityObservedId);
    if (obj) {
        /* Flag for OnDeleteTarget policies */
        flecs_add_flag(world, obj, EcsEntityObservedObject);
        if (ecs_has_id(world, rel, EcsAcyclic)) {
            /* Flag used to determine if object should be traversed when
             * propagating events or with super/subset queries */
            flecs_add_flag(world, obj, EcsEntityObservedAcyclic);
        }
    }

    /* Flags for events */
    if (flecs_check_observers_for_event(world, id, EcsOnAdd)) {
        idr->flags |= EcsIdHasOnAdd;
    }
    if (flecs_check_observers_for_event(world, id, EcsOnRemove)) {
        idr->flags |= EcsIdHasOnRemove;
    }
    if (flecs_check_observers_for_event(world, id, EcsOnSet)) {
        idr->flags |= EcsIdHasOnSet;
    }
    if (flecs_check_observers_for_event(world, id, EcsUnSet)) {
        idr->flags |= EcsIdHasUnSet;
    }

    if (ecs_should_log_1()) {
        char *id_str = ecs_id_str(world, id);
        ecs_dbg_1("#[green]id#[normal] %s #[green]created", id_str);
        ecs_os_free(id_str);
    }

    /* Update counters */
    world->info.id_create_total ++;

    if (!is_wildcard) {
        world->info.id_count ++;

        if (idr->type_info) {
            world->info.component_id_count ++;
        } else {
            world->info.tag_id_count ++;
        }

        if (ECS_HAS_ROLE(id, PAIR)) {
            world->info.pair_id_count ++;
        }
    } else {
        world->info.wildcard_id_count ++;
    }

    return idr;
error:
    return NULL;
}

static
void flecs_id_record_assert_empty(
    ecs_id_record_t *idr)
{
    (void)idr;
    ecs_assert(flecs_table_cache_count(&idr->cache) == 0, 
        ECS_INTERNAL_ERROR, NULL);
    ecs_assert(flecs_table_cache_empty_count(&idr->cache) == 0, 
        ECS_INTERNAL_ERROR, NULL);
}

static
void flecs_id_record_free(
    ecs_world_t *world,
    ecs_id_record_t *idr)
{
    ecs_poly_assert(world, ecs_world_t);
    ecs_assert(idr != NULL, ECS_INTERNAL_ERROR, NULL);
    ecs_id_t id = idr->id;

    flecs_id_record_assert_empty(idr);

    if (ECS_HAS_ROLE(id, PAIR)) {
        ecs_entity_t rel = ecs_pair_first(world, id);
        ecs_entity_t obj = ECS_PAIR_SECOND(id);
        if (!ecs_id_is_wildcard(id)) {
            /* If id is not a wildcard, remove it from the wildcard lists */
            remove_id_elem(idr, ecs_pair(rel, EcsWildcard));
            remove_id_elem(idr, ecs_pair(EcsWildcard, obj));
        } else {
            ecs_log_push_2();

            /* If id is a wildcard, it means that all id records that match the
             * wildcard are also empty, so release them */
            if (ECS_PAIR_FIRST(id) == EcsWildcard) {
                /* Iterate (*, Target) list */
                ecs_id_record_t *cur, *next = idr->second.next;
                while ((cur = next)) {
                    flecs_id_record_assert_empty(cur);
                    next = cur->second.next;
                    flecs_id_record_release(world, cur);
                }
            } else {
                /* Iterate (Relationship, *) list */
                ecs_assert(ECS_PAIR_SECOND(id) == EcsWildcard, 
                    ECS_INTERNAL_ERROR, NULL);
                ecs_id_record_t *cur, *next = idr->first.next;
                while ((cur = next)) {
                    flecs_id_record_assert_empty(cur);
                    next = cur->first.next;
                    flecs_id_record_release(world, cur);
                }
            }

            ecs_log_pop_2();
        }
    }

    /* Update counters */
    world->info.id_delete_total ++;

    if (!ecs_id_is_wildcard(id)) {
        world->info.id_count --;

        if (ECS_HAS_ROLE(id, PAIR)) {
            world->info.pair_id_count --;
        }

        if (idr->type_info) {
            world->info.component_id_count --;
        } else {
            world->info.tag_id_count --;
        }
    } else {
        world->info.wildcard_id_count --;
    }

    /* Unregister the id record from the world */
    ecs_map_remove(&world->id_index, flecs_id_record_id(id));

    /* Free resources */
    ecs_table_cache_fini(&idr->cache);
    flecs_name_index_free(idr->name_index);
    ecs_os_free(idr);

    if (ecs_should_log_1()) {
        char *id_str = ecs_id_str(world, id);
        ecs_dbg_1("#[green]id#[normal] %s #[red]deleted", id_str);
        ecs_os_free(id_str);
    }
}

ecs_id_record_t* flecs_id_record_ensure(
    ecs_world_t *world,
    ecs_id_t id)
{
    ecs_poly_assert(world, ecs_world_t);

    ecs_id_record_t **idr_ptr = ecs_map_ensure(&world->id_index, 
        ecs_id_record_t*, ecs_strip_generation(id));
    ecs_id_record_t *idr = idr_ptr[0];
    if (!idr) {
        idr = flecs_id_record_new(world, id);
        idr_ptr = ecs_map_get(&world->id_index, 
            ecs_id_record_t*, flecs_id_record_id(id));
        ecs_assert(idr_ptr != NULL, ECS_INTERNAL_ERROR, NULL);
        idr_ptr[0] = idr;
    }

    return idr;
}

ecs_id_record_t* flecs_id_record_get(
    const ecs_world_t *world,
    ecs_id_t id)
{
    ecs_poly_assert(world, ecs_world_t);
    return ecs_map_get_ptr(&world->id_index, ecs_id_record_t*,
        flecs_id_record_id(id));
}

ecs_id_record_t* flecs_query_id_record_get(
    const ecs_world_t *world,
    ecs_id_t id)
{
    ecs_id_record_t *idr = flecs_id_record_get(world, id);
    if (!idr) {
        if (ECS_HAS_ROLE(id, PAIR)) {
            idr = flecs_id_record_get(world,
                ecs_pair(EcsUnion, ECS_PAIR_FIRST(id)));
        }
        return idr;
    }
    if (ECS_HAS_ROLE(id, PAIR) && 
        ECS_PAIR_SECOND(id) == EcsWildcard && 
        (idr->flags & EcsIdUnion)) 
    {
        idr = flecs_id_record_get(world, 
            ecs_pair(EcsUnion, ECS_PAIR_FIRST(id)));
    }

    return idr;
}

void flecs_id_record_claim(
    ecs_world_t *world,
    ecs_id_record_t *idr)
{
    (void)world;
    idr->refcount ++;
}

int32_t flecs_id_record_release(
    ecs_world_t *world,
    ecs_id_record_t *idr)
{
    int32_t rc = -- idr->refcount;
    ecs_assert(rc >= 0, ECS_INTERNAL_ERROR, NULL);

    if (!rc) {
        flecs_id_record_free(world, idr);
    }

    return rc;
}

void flecs_id_record_release_tables(
    ecs_world_t *world,
    ecs_id_record_t *idr)
{
    /* Cache should not contain tables that aren't empty */
    ecs_assert(flecs_table_cache_count(&idr->cache) == 0, 
        ECS_INTERNAL_ERROR, NULL);

    ecs_table_cache_iter_t it;
    if (flecs_table_cache_empty_iter(&idr->cache, &it)) {
        ecs_table_record_t *tr;
        while ((tr = flecs_table_cache_next(&it, ecs_table_record_t))) {
            /* Tables can hold claims on each other, so releasing a table can
             * cause the next element to get invalidated. Claim the next table
             * so that we can safely iterate. */
            ecs_table_t *next = NULL;
            if (it.next) {
                next = it.next->table;
                flecs_table_claim(world, next);
            }

            /* Release current table */
            flecs_table_release(world, tr->hdr.table);

            /* Check if the only thing keeping the next table alive is our
             * claim. If so, move to the next record before releasing */
            if (next) {
                if (next->refcount == 1) {
                    it.next = it.next->next;
                }

                flecs_table_release(world, next);
            }
        }
    }
}

bool flecs_id_record_set_type_info(
    ecs_world_t *world,
    ecs_id_record_t *idr,
    const ecs_type_info_t *ti)
{
    if (!ecs_id_is_wildcard(idr->id)) {
        if (ti) {
            if (!idr->type_info) {
                world->info.tag_id_count --;
                world->info.component_id_count ++;
            }
        } else {
            if (idr->type_info) {
                world->info.tag_id_count ++;
                world->info.component_id_count --;
            }
        }
    }

    bool changed = idr->type_info != ti;
    idr->type_info = ti;
    return changed;
}

ecs_hashmap_t* flecs_id_name_index_ensure(
    ecs_world_t *world,
    ecs_id_t id)
{
    ecs_poly_assert(world, ecs_world_t);

    ecs_id_record_t *idr = flecs_id_record_get(world, id);
    ecs_assert(idr != NULL, ECS_INTERNAL_ERROR, NULL);

    ecs_hashmap_t *map = idr->name_index;
    if (!map) {
        map = idr->name_index = flecs_name_index_new();
    }

    return map;
}

ecs_hashmap_t* flecs_id_name_index_get(
    const ecs_world_t *world,
    ecs_id_t id)
{
    ecs_poly_assert(world, ecs_world_t);

    ecs_id_record_t *idr = flecs_id_record_get(world, id);
    if (!idr) {
        return NULL;
    }

    return idr->name_index;
}

ecs_table_record_t* flecs_table_record_get(
    const ecs_world_t *world,
    const ecs_table_t *table,
    ecs_id_t id)
{
    ecs_poly_assert(world, ecs_world_t);

    ecs_id_record_t* idr = flecs_id_record_get(world, id);
    if (!idr) {
        return NULL;
    }

    return (ecs_table_record_t*)ecs_table_cache_get(&idr->cache, table);
}

const ecs_table_record_t* flecs_id_record_get_table(
    const ecs_id_record_t *idr,
    const ecs_table_t *table)
{
    if (!idr) {
        return NULL;
    }
    return (ecs_table_record_t*)ecs_table_cache_get(&idr->cache, table);
}

void flecs_fini_id_records(
    ecs_world_t *world)
{
    ecs_map_iter_t it = ecs_map_iter(&world->id_index);
    ecs_id_record_t *idr;
    while ((idr = ecs_map_next_ptr(&it, ecs_id_record_t*, NULL))) {
        flecs_id_record_release(world, idr);
    }

    ecs_assert(ecs_map_count(&world->id_index) == 0, ECS_INTERNAL_ERROR, NULL);

    ecs_map_fini(&world->id_index);
    flecs_sparse_free(world->pending_tables);
    flecs_sparse_free(world->pending_buffer);
}

