#include <api.h>

void Internals_setup(void) {
    ecs_log_set_level(-3);
}

static
void Iter(ecs_iter_t *it) {
    Position *p = ecs_field(it, Position, 1);
    Velocity *v = ecs_field(it, Velocity, 2);
    Mass *m = ecs_field(it, Mass, 3);

    probe_iter(it);

    int i;
    for (i = 0; i < it->count; i ++) {
        p[i].x = 10;
        p[i].y = 20;

        if (v) {
            v[i].x = 30;
            v[i].y = 40;
        }

        if (m) {
            m[i] = 50;
        }
    }
}

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

    ECS_COMPONENT(world, Position);

    ECS_ENTITY(world, e1, Position);
    ECS_ENTITY(world, e2, Position);

    ECS_SYSTEM(world, Iter, EcsOnUpdate, Position);

    /* System is now matched with archetype of entities. Delete entities to
     * deactivate table for system */
    ecs_delete(world, e1);
    ecs_delete(world, e2);

    test_assert(true);

    ecs_fini(world);
}

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

    ECS_COMPONENT(world, Position);

    ECS_SYSTEM(world, Iter, EcsOnUpdate, Position);

    /* Add entities after system definition to trigger table activation */
    ECS_ENTITY(world, e1, Position);
    ECS_ENTITY(world, e2, Position);

    test_assert(true);

    ecs_fini(world);
}

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

    ECS_COMPONENT(world, Position);

    ECS_SYSTEM(world, Iter, EcsOnUpdate, Position);

    /* Add entities after system definition to trigger table activation */
    ECS_ENTITY(world, e1, Position);
    ECS_ENTITY(world, e2, Position);

    /* System is now matched with archetype of entities. Delete entities to
     * deactivate table for system */
    ecs_delete(world, e1);
    ecs_delete(world, e2);

    test_assert(true);

    ecs_fini(world);
}

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

    ECS_COMPONENT(world, Position);

    ECS_SYSTEM(world, Iter, EcsOnUpdate, Position);

    /* Add entities after system definition to trigger table activation */
    ECS_ENTITY(world, e1, Position);
    ECS_ENTITY(world, e2, Position);

    /* System is now matched with archetype of entities. Delete entities to
     * deactivate table for system */
    ecs_delete(world, e1);
    ecs_delete(world, e2);

    /* Add entities of same type to trigger table reactivation */
    ECS_ENTITY(world, e3, Position);
    ECS_ENTITY(world, e4, Position);

    test_assert(true);

    ecs_fini(world);
}

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

    ECS_COMPONENT(world, Position);
    ECS_COMPONENT(world, Velocity);

    ECS_SYSTEM(world, Iter, EcsOnUpdate, Position);

    /* Add entities after system definition to trigger table activation */
    ECS_ENTITY(world, e1, Position);
    ECS_ENTITY(world, e2, Position);

    /* System is now matched with archetype of entities. Delete entities to
     * deactivate table for system */
    ecs_delete(world, e1);
    ecs_delete(world, e2);

    /* Add entities of different type to trigger new table activation */
    ECS_ENTITY(world, e3, Position, Velocity);
    ECS_ENTITY(world, e4, Position, Velocity);

    test_assert(true);

    ecs_fini(world);
}

static int invoked = 0;

static
void CreateNewTable(ecs_iter_t *it) {
    ecs_id_t ecs_id(Velocity) = ecs_field_id(it, 2);

    int32_t i;
    for (i = 0; i < it->count; i ++) {
        ecs_add(it->world, it->entities[i], Velocity);
    }
}

static
void ManualSystem(ecs_iter_t *it) {
    invoked ++;
}

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

    ECS_COMPONENT(world, Position);
    ECS_COMPONENT(world, Velocity);

    ECS_ENTITY(world, e, Position);

    ECS_SYSTEM(world, CreateNewTable, EcsOnUpdate, Position, Velocity());
    ECS_SYSTEM(world, ManualSystem, 0, Position, Velocity);

    /* CreateNewTable system created a new, non-empty table. This will be merged
     * which will trigger activation of ManualSystem. This will cause the system
     * to go from the inactive_systems array to the manual_systems array. This
     * happens as systems are notified of new tables (during the merge). Because
     * the manual_systems array was evaluated after the inactive_systems array,
     * a table could be added to a system twice. */
    ecs_progress(world, 0);

    /* Validate that the CreateNewTable system has ran */
    test_assert(ecs_has(world, e, Position));
    test_assert(ecs_has(world, e, Velocity));

    /* Now run the ManualSystem, and make sure it is only invoked once. If it is
     * invoked twice, the table has been registered with the system twice, which
     * is wrong. */
    ecs_run(world, ManualSystem, 0, NULL);

    test_int(invoked, 1);

    ecs_fini(world);
}

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

    ECS_COMPONENT(world, Position);

    ecs_entity_t parent_A = ecs_new(world, 0);
    ecs_entity_t child_A = ecs_new_w_pair(world, EcsChildOf, parent_A);
    test_assert(parent_A != 0);
    test_assert(child_A != 0);

    ecs_delete(world, parent_A); // Deletes table
    test_assert( !ecs_is_alive(world, parent_A));
    test_assert( !ecs_is_alive(world, child_A));

    ecs_entity_t parent_B = ecs_new(world, 0);
    ecs_entity_t child_B = ecs_new_w_pair(world, EcsChildOf, parent_B);
    test_assert(parent_B != 0);
    test_assert(child_B != 0);
    
    ecs_fini(world);
}

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

    int32_t i;
    for (i = 0; i <= 65536; i ++) {
        ecs_entity_t e = ecs_new_id(world);
        ecs_add_id(world, e, e);
        test_assert(ecs_has_id(world, e, e));
        test_int(ecs_get_type(world, e)->count, 1);
    }
    
    ecs_fini(world);
}

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

    /* This scenario triggered a bug where the first table registered in the
     * world would get id 0, which is the same id as the root table. This caused
     * the table cache to assert as it saw the wrong table for an id. */

    int32_t i;
    for (i = 0; i <= 50; i ++) {
        ecs_entity_t e = ecs_new_id(world);
        ecs_add_id(world, e, i + 1000);
        test_assert(e != 0);
        test_assert(ecs_has_id(world, e, i + 1000));
    }

    ecs_entity_t f = ecs_entity_init(world, &(ecs_entity_desc_t){
        .name = "Foo"
    });
    
    test_assert(f != 0);
    test_str(ecs_get_name(world, f), "Foo");
    
    ecs_fini(world);
}

void Internals_override_os_api_w_addon(void) {
    ecs_os_set_api_defaults();
    ecs_os_api_t os_api = ecs_os_api;

    ecs_os_set_api(&os_api);

    test_assert(ecs_os_has_threading());
    test_assert(ecs_os_has_time());
    test_assert(ecs_os_has_logging());
    test_assert(ecs_os_has_heap());

    ecs_world_t *world = ecs_init();
    ecs_fini(world);
}

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

    ecs_entity_t TagA = ecs_new_id(world);
    ecs_entity_t TagB = ecs_new_id(world);
    ecs_entity_t TagC = ecs_new_id(world);
    ecs_entity_t TagD = ecs_new_id(world);
    ecs_entity_t TagE = ecs_new_id(world);
    ecs_entity_t TagF = ecs_new_id(world);
    ecs_entity_t TagG = ecs_new_id(world);
    ecs_entity_t TagH = ecs_new_id(world);

    ecs_entity_t RelA = ecs_new_id(world);
    ecs_entity_t RelB = ecs_new_id(world);
    ecs_entity_t RelC = ecs_new_id(world);
    ecs_entity_t RelD = ecs_new_id(world);
    ecs_entity_t RelE = ecs_new_id(world);
    ecs_entity_t RelF = ecs_new_id(world);
    ecs_entity_t RelG = ecs_new_id(world);
    ecs_entity_t TgtA = ecs_new_id(world);
    ecs_entity_t TgtB = ecs_new_id(world);

    ecs_entity_t e = ecs_new_id(world);
    ecs_add_id(world, e, TagA);
    ecs_add_id(world, e, TagB);
    ecs_add_id(world, e, TagC);
    ecs_add_id(world, e, TagD);
    ecs_add_id(world, e, TagE);
    ecs_add_id(world, e, TagF);
    ecs_add_id(world, e, TagG);
    ecs_add_id(world, e, TagH);
    ecs_add_pair(world, e, RelA, TgtA);
    ecs_add_pair(world, e, RelB, TgtA);
    ecs_add_pair(world, e, RelC, TgtA);
    ecs_add_pair(world, e, RelD, TgtA);
    ecs_add_pair(world, e, RelE, TgtA);
    ecs_add_pair(world, e, RelF, TgtA);
    ecs_override_pair(world, e, RelG, TgtB);

    test_assert(ecs_has_id(world, e, TagA));
    test_assert(ecs_has_id(world, e, TagB));
    test_assert(ecs_has_id(world, e, TagC));
    test_assert(ecs_has_id(world, e, TagD));
    test_assert(ecs_has_id(world, e, TagE));
    test_assert(ecs_has_id(world, e, TagF));
    test_assert(ecs_has_id(world, e, TagG));
    test_assert(ecs_has_id(world, e, TagH));
    test_assert(ecs_has_pair(world, e, RelA, TgtA));
    test_assert(ecs_has_pair(world, e, RelB, TgtA));
    test_assert(ecs_has_pair(world, e, RelC, TgtA));
    test_assert(ecs_has_pair(world, e, RelD, TgtA));
    test_assert(ecs_has_pair(world, e, RelE, TgtA));
    test_assert(ecs_has_pair(world, e, RelF, TgtA));

    ecs_fini(world);
}

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

    ECS_TAG(world, TagA);
    ECS_TAG(world, TagB);

    ecs_entity_t p = ecs_new_id(world);
    ecs_entity_t c = ecs_new_w_pair(world, EcsChildOf, p);
    ecs_table_t *pt = ecs_get_table(world, p);
    ecs_table_t *ct = ecs_get_table(world, c);
    test_assert(pt == NULL);
    test_assert(ct != NULL);
    test_int(flecs_table_observed_count(ct), 0);

    ecs_add(world, p, TagA);
    ecs_table_t *pt_a = ecs_get_table(world, p);
    test_assert(pt_a != NULL);
    test_int(flecs_table_observed_count(pt_a), 1);

    ecs_add(world, p, TagB);
    ecs_table_t *pt_b = ecs_get_table(world, p);
    test_assert(pt_b != NULL);
    test_int(flecs_table_observed_count(pt_a), 0);
    test_int(flecs_table_observed_count(pt_b), 1);

    ecs_fini(world);
}

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

    ECS_TAG(world, TagA);
    ECS_TAG(world, TagB);

    ecs_entity_t p = ecs_new_id(world);
    ecs_entity_t c = ecs_new_w_pair(world, EcsChildOf, p);
    ecs_table_t *pt = ecs_get_table(world, p);
    ecs_table_t *ct = ecs_get_table(world, c);
    test_assert(pt == NULL);
    test_assert(ct != NULL);
    test_int(flecs_table_observed_count(ct), 0);

    ecs_add(world, p, TagA);
    ecs_table_t *pt_a = ecs_get_table(world, p);
    test_assert(pt_a != NULL);
    test_int(flecs_table_observed_count(pt_a), 1);

    ecs_add(world, p, TagB);
    ecs_table_t *pt_b = ecs_get_table(world, p);
    test_assert(pt_b != NULL);
    test_int(flecs_table_observed_count(pt_a), 0);
    test_int(flecs_table_observed_count(pt_b), 1);

    ecs_remove(world, p, TagB);
    test_int(flecs_table_observed_count(pt_a), 1);
    test_int(flecs_table_observed_count(pt_b), 0);

    ecs_remove(world, p, TagA);
    test_int(flecs_table_observed_count(pt_a), 0);
    test_int(flecs_table_observed_count(pt_b), 0);

    ecs_fini(world);
}

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

    ECS_TAG(world, TagA);

    ecs_entity_t p = ecs_new_id(world);
    ecs_entity_t c = ecs_new_w_pair(world, EcsChildOf, p);
    ecs_table_t *pt = ecs_get_table(world, p);
    ecs_table_t *ct = ecs_get_table(world, c);
    test_assert(pt == NULL);
    test_assert(ct != NULL);
    test_int(flecs_table_observed_count(ct), 0);

    ecs_add(world, p, TagA);
    ecs_table_t *pt_a = ecs_get_table(world, p);
    test_assert(pt_a != NULL);
    test_int(flecs_table_observed_count(pt_a), 1);

    ecs_clear(world, p);
    test_int(flecs_table_observed_count(pt_a), 0);

    ecs_add(world, p, TagA);
    test_int(flecs_table_observed_count(pt_a), 1);

    ecs_fini(world);
}

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

    ECS_TAG(world, TagA);

    ecs_entity_t p = ecs_new_id(world);
    ecs_entity_t c = ecs_new_w_pair(world, EcsChildOf, p);
    ecs_table_t *pt = ecs_get_table(world, p);
    ecs_table_t *ct = ecs_get_table(world, c);
    test_assert(pt == NULL);
    test_assert(ct != NULL);
    test_int(flecs_table_observed_count(ct), 0);

    ecs_add(world, p, TagA);
    ecs_table_t *pt_a = ecs_get_table(world, p);
    test_assert(pt_a != NULL);
    test_int(flecs_table_observed_count(pt_a), 1);

    ecs_delete(world, p);
    test_int(flecs_table_observed_count(pt_a), 0);

    ecs_fini(world);
}

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

    ECS_TAG(world, TagA);

    ecs_entity_t p = ecs_new_id(world);
    ecs_entity_t c = ecs_new_w_pair(world, EcsChildOf, p);
    ecs_table_t *pt = ecs_get_table(world, p);
    ecs_table_t *ct = ecs_get_table(world, c);
    test_assert(pt == NULL);
    test_assert(ct != NULL);
    test_int(flecs_table_observed_count(ct), 0);

    ecs_entity_t t = ecs_new_id(world);
    ecs_add_id(world, p, t);
    ecs_table_t *pt_t = ecs_get_table(world, p);
    test_assert(pt_t != NULL);
    test_int(flecs_table_observed_count(pt_t), 1);

    ecs_add(world, p, TagA);
    ecs_table_t *pt_ta = ecs_get_table(world, p);
    test_assert(pt_ta != NULL);
    test_int(flecs_table_observed_count(pt_t), 0);
    test_int(flecs_table_observed_count(pt_ta), 1);

    ecs_delete(world, t);
    test_assert(!ecs_has_id(world, p, t));
    test_assert(ecs_has(world, p, TagA));

    ecs_table_t *p_a = ecs_get_table(world, p);
    test_assert(p_a != NULL);
    test_int(flecs_table_observed_count(p_a), 1);

    ecs_fini(world);
}

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

    ECS_TAG(world, TagA);
    ECS_TAG(world, TagB);

    ecs_entity_t p = ecs_new_id(world);

    ecs_add(world, p, TagA);
    ecs_table_t *p_a = ecs_get_table(world, p);
    test_assert(p_a != NULL);
    test_int(flecs_table_observed_count(p_a), 0);

    ecs_entity_t c = ecs_new_w_pair(world, EcsChildOf, p);
    ecs_table_t *ct = ecs_get_table(world, c);
    test_assert(ct != NULL);
    test_int(flecs_table_observed_count(ct), 0);
    test_int(flecs_table_observed_count(p_a), 1);

    ecs_fini(world);

}

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

    int64_t max_block_count;

    ecs_entity_t tag = ecs_new_id(world);
    ecs_entity_t e = ecs_new_w_id(world, tag);
    max_block_count = ecs_block_allocator_alloc_count - 
        ecs_block_allocator_free_count;
    ecs_delete(world, tag);

    for (int i = 0; i < 25000; i ++) {
        tag = ecs_new_id(world);
        ecs_add_id(world, e, tag);
        ecs_delete(world, tag);
    }

    test_int(max_block_count, ecs_block_allocator_alloc_count - 
        ecs_block_allocator_free_count);

    ecs_fini(world);
}
