#include <api.h>

static ECS_COMPONENT_DECLARE(Position);
static ECS_DECLARE(Tag);

static
void test_no_chain(
    ecs_world_t *world,
    ecs_poly_t *poly,
    ecs_id_t filter)
{
    ecs_entity_t e1 = ecs_set(world, 0, Position, {10, 20});
    ecs_entity_t e2 = ecs_set(world, 0, Position, {30, 40});
    ecs_entity_t e3 = ecs_set(world, 0, Position, {50, 60});

    ecs_add(world, e3, Tag);

    ecs_iter_t it;
    if (filter) {
        ecs_term_t term = { .id = filter };
        ecs_iter_poly(world, poly, &it, &term);
    } else {
        ecs_iter_poly(world, poly, &it, NULL);
    }

    test_assert(ecs_iter_next(&it));
    test_int(it.count, 2);
    test_int(it.entities[0], e1);
    test_int(it.entities[1], e2);
    test_int(ecs_field_id(&it, 1), ecs_id(Position));
    test_int(ecs_field_src(&it, 1), 0);
    test_assert(it.table != NULL);

    Position *ptr = ecs_field(&it, Position, 1);
    test_assert(ptr != NULL);
    test_int(ptr[0].x, 10);
    test_int(ptr[0].y, 20);
    test_int(ptr[1].x, 30);
    test_int(ptr[1].y, 40);

    test_assert(ecs_iter_next(&it));
    test_int(it.count, 1);
    test_int(it.entities[0], e3);
    test_int(ecs_field_id(&it, 1), ecs_id(Position));
    test_int(ecs_field_src(&it, 1), 0);
    test_assert(it.table != NULL);

    ptr = ecs_field(&it, Position, 1);
    test_assert(ptr != NULL);
    test_int(ptr[0].x, 50);
    test_int(ptr[0].y, 60);

    test_assert(!ecs_iter_next(&it));
}

static
void test_w_chain(
    ecs_world_t *world,
    ecs_poly_t *poly)
{
    ecs_entity_t e1 = ecs_set(world, 0, Position, {10, 20});
    ecs_entity_t e2 = ecs_set(world, 0, Position, {30, 40});
    ecs_set(world, 0, Position, {50, 60}); // Not matched

    ecs_add(world, e1, Tag);
    ecs_add(world, e2, Tag);

    ecs_iter_t it_arr[2];
    ecs_iter_t *it = it_arr;
    ecs_iter_poly(world, poly, it_arr, &(ecs_term_t){ .id = Tag});

    test_assert(ecs_iter_next(it));
    test_int(it->count, 2);
    test_int(it->entities[0], e1);
    test_int(it->entities[1], e2);
    test_int(ecs_field_id(it, 1), Tag);
    test_int(ecs_field_src(it, 1), 0);
    test_assert(it->table != NULL);

    ecs_iter_t *chain_it = it->chain_it;
    test_assert(chain_it != NULL);
    
    test_int(chain_it->count, 2);
    test_int(chain_it->entities[0], e1);
    test_int(chain_it->entities[1], e2);
    test_int(ecs_field_id(chain_it, 1), ecs_id(Position));
    test_int(ecs_field_src(chain_it, 1), 0);

    Position *ptr = ecs_field(chain_it, Position, 1);
    test_assert(ptr != NULL);
    test_int(ptr[0].x, 10);
    test_int(ptr[0].y, 20);
    test_int(ptr[1].x, 30);
    test_int(ptr[1].y, 40);

    test_assert(!ecs_iter_next(it));
}

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

    ECS_COMPONENT_DEFINE(world, Position);
    ECS_TAG_DEFINE(world, Tag);
    
    ecs_query_t *q = ecs_query_new(world, "Position");
    test_assert(q != NULL);

    test_no_chain(world, q, 0);

    ecs_fini(world);
}

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

    ECS_COMPONENT_DEFINE(world, Position);
    ECS_TAG_DEFINE(world, Tag);
    
    ecs_query_t *q = ecs_query_new(world, "Position");
    test_assert(q != NULL);

    test_w_chain(world, q);

    ecs_fini(world);
}

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

    ECS_COMPONENT_DEFINE(world, Position);
    ECS_TAG_DEFINE(world, Tag);

    ecs_entity_t e1 = ecs_set(world, 0, Position, {10, 20});
    ecs_entity_t e2 = ecs_set(world, 0, Position, {30, 40});
    ecs_entity_t e3 = ecs_set(world, 0, Position, {50, 60});
    ecs_add(world, e3, Tag);

    bool e1_found = false;
    bool e2_found = false;
    bool e3_found = false;
    bool position_found = false;
    bool tag_found = false;

    ecs_iter_t it;
    ecs_iter_poly(world, world, &it, 0);

    test_assert(ecs_iter_next(&it));
    test_assert(it.count != 0); 

    int i;
    for (i = 0; i < it.count; i ++) {
        test_assert( ecs_is_alive(world, it.entities[i]));
        if (it.entities[i] == e1) e1_found = true;
        if (it.entities[i] == e2) e2_found = true;
        if (it.entities[i] == e3) e3_found = true;
        if (it.entities[i] == ecs_id(Position)) position_found = true;
        if (it.entities[i] == Tag) tag_found = true;
    }

    test_assert(!ecs_iter_next(&it));

    test_assert(e1_found);
    test_assert(e2_found);
    test_assert(e3_found);
    test_assert(position_found);
    test_assert(tag_found);

    ecs_fini(world);
}

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

    ECS_COMPONENT_DEFINE(world, Position);
    ECS_TAG_DEFINE(world, Tag);

    test_no_chain(world, world, ecs_id(Position));

    ecs_fini(world);
}

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

    ECS_COMPONENT_DEFINE(world, Position);
    ECS_TAG_DEFINE(world, Tag);
    
    ecs_rule_t *q = ecs_rule_new(world, "Position");
    test_assert(q != NULL);

    test_no_chain(world, q, 0);

    ecs_rule_fini(q);

    ecs_fini(world);
}

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

    ECS_COMPONENT_DEFINE(world, Position);
    ECS_TAG_DEFINE(world, Tag);
    
    ecs_rule_t *q = ecs_rule_new(world, "Position");
    test_assert(q != NULL);

    test_w_chain(world, q);

    ecs_rule_fini(q);

    ecs_fini(world);
}

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

    ECS_COMPONENT_DEFINE(world, Position);
    ECS_TAG_DEFINE(world, Tag);

    ecs_filter_t *f = ecs_filter_init(world, &(ecs_filter_desc_t){
        .expr = "Position"});
    test_assert(f != NULL);

    test_no_chain(world, f, 0);

    ecs_filter_fini(f);

    ecs_fini(world);
}

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

    ECS_COMPONENT_DEFINE(world, Position);
    ECS_TAG_DEFINE(world, Tag);
    
    ecs_filter_t *f = ecs_filter_init(world, &(ecs_filter_desc_t){
        .expr = "Position"});
    test_assert(f != NULL);

    test_w_chain(world, f);

    ecs_filter_fini(f);

    ecs_fini(world);
}

static
void FooTrigger(ecs_iter_t *it) {}

static
void PolyTrigger(ecs_iter_t *it) {
    probe_system_w_ctx(it, it->ctx);

    EcsPoly *poly = ecs_field(it, EcsPoly, 1);

    test_int(1, it->count);
    test_assert(poly->poly != NULL);
}

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

    ecs_entity_t tag = ecs_new_id(world);

    Probe ctx = {0};
    ecs_observer_init(world, &(ecs_observer_desc_t){
        .filter.terms[0] = { ecs_pair(ecs_id(EcsPoly), EcsObserver) },
        .events = { EcsOnSet },
        .callback = PolyTrigger,
        .ctx = &ctx
    });

    test_int(1, ctx.invoked);

    ecs_os_zeromem(&ctx);

    ecs_entity_t t = ecs_observer_init(world, &(ecs_observer_desc_t){
        .filter.terms = {{ tag }},
        .events = { EcsOnAdd },
        .callback = FooTrigger
    });

    test_int(1, ctx.invoked);
    test_int(1, ctx.count);
    test_uint(t, ctx.e[0]);

    ecs_fini(world);
}

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

    ecs_entity_t tag = ecs_new_id(world);

    Probe ctx = {0};
    ecs_observer_init(world, &(ecs_observer_desc_t){
        .filter.terms[0] = { ecs_pair(ecs_id(EcsPoly), EcsQuery) },
        .events = { EcsOnSet },
        .callback = PolyTrigger,
        .ctx = &ctx
    });

    test_int(0, ctx.invoked);

    ecs_query_init(world, &(ecs_query_desc_t){
        .filter.terms = {{ tag }},
    });

    test_int(1, ctx.invoked);
    test_int(1, ctx.count);

    ecs_fini(world);
}

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

    ecs_entity_t tag = ecs_new_id(world);

    Probe ctx = {0};
    ecs_observer_init(world, &(ecs_observer_desc_t){
        .filter.terms[0] = { ecs_pair(ecs_id(EcsPoly), EcsSystem) },
        .events = { EcsOnSet },
        .callback = PolyTrigger,
        .ctx = &ctx
    });

    test_int(0, ctx.invoked);

    ecs_entity_t s = ecs_system_init(world, &(ecs_system_desc_t){
        .query.filter.terms = {{ tag }},
        .callback = FooTrigger
    });

    test_int(1, ctx.invoked);
    test_int(1, ctx.count);
    test_uint(s, ctx.e[0]);

    ecs_fini(world);
}

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

    ECS_TAG(world, Tag);

    ecs_entity_t e = ecs_new(world, Tag);

    ecs_entity_t qe = ecs_new_id(world);
    ecs_filter_t *f = ecs_filter(world, {
        .terms = {{ Tag }},
        .entity = qe
    });

    test_assert(f->entity == qe);

    const EcsPoly *poly = ecs_get_pair(world, qe, EcsPoly, EcsQuery);
    test_assert(poly != NULL);
    test_assert(poly->poly == f);

    ecs_iter_t it;
    ecs_iter_poly(world, poly->poly, &it, NULL);

    test_bool(true, ecs_iter_next(&it));
    test_int(it.count, 1);
    test_uint(it.entities[0], e);
    test_bool(false, ecs_iter_next(&it));

    ecs_filter_fini(f);

    test_assert(!ecs_is_alive(world, qe));

    ecs_fini(world);
}

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

    ECS_TAG(world, Tag);

    ecs_entity_t e = ecs_new(world, Tag);

    ecs_entity_t qe = ecs_new_id(world);
    ecs_query_t *q = ecs_query(world, {
        .filter.terms = {{ Tag }},
        .filter.entity = qe
    });

    const EcsPoly *poly = ecs_get_pair(world, qe, EcsPoly, EcsQuery);
    test_assert(poly != NULL);
    test_assert(poly->poly == q);

    ecs_iter_t it;
    ecs_iter_poly(world, poly->poly, &it, NULL);

    test_bool(true, ecs_iter_next(&it));
    test_int(it.count, 1);
    test_uint(it.entities[0], e);
    test_bool(false, ecs_iter_next(&it));

    ecs_query_fini(q);

    test_assert(!ecs_is_alive(world, qe));

    ecs_fini(world);
}

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

    ECS_TAG(world, Tag);

    ecs_entity_t e = ecs_new(world, Tag);

    ecs_entity_t qe = ecs_new_id(world);
    ecs_rule_t *q = ecs_rule(world, {
        .terms = {{ Tag }},
        .entity = qe
    });

    const EcsPoly *poly = ecs_get_pair(world, qe, EcsPoly, EcsQuery);
    test_assert(poly != NULL);
    test_assert(poly->poly == q);

    ecs_iter_t it;
    ecs_iter_poly(world, poly->poly, &it, NULL);

    test_bool(true, ecs_iter_next(&it));
    test_int(it.count, 1);
    test_uint(it.entities[0], e);
    test_bool(false, ecs_iter_next(&it));

    ecs_rule_fini(q);

    test_assert(!ecs_is_alive(world, qe));

    ecs_fini(world);
}

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

    ECS_TAG(world, Tag);

    ecs_entity_t qe = ecs_new_id(world);
    ecs_filter_t *f = ecs_filter(world, {
        .terms = {{ Tag }},
        .entity = qe
    });

    test_assert(f->entity == qe);

    const EcsPoly *poly = ecs_get_pair(world, qe, EcsPoly, EcsQuery);
    test_assert(poly != NULL);
    test_assert(poly->poly == f);

    ecs_delete(world, qe);

    ecs_fini(world);
}

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

    ECS_TAG(world, Tag);

    ecs_entity_t qe = ecs_new_id(world);
    ecs_query_t *q = ecs_query(world, {
        .filter.terms = {{ Tag }},
        .filter.entity = qe
    });

    const EcsPoly *poly = ecs_get_pair(world, qe, EcsPoly, EcsQuery);
    test_assert(poly != NULL);
    test_assert(poly->poly == q);

    ecs_delete(world, qe);

    test_assert(!ecs_is_alive(world, qe));

    ecs_fini(world);
}

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

    ECS_TAG(world, Tag);

    ecs_entity_t qe = ecs_new_id(world);
    ecs_rule_t *q = ecs_rule(world, {
        .terms = {{ Tag }},
        .entity = qe
    });

    const EcsPoly *poly = ecs_get_pair(world, qe, EcsPoly, EcsQuery);
    test_assert(poly != NULL);
    test_assert(poly->poly == q);

    ecs_delete(world, qe);

    ecs_fini(world);
}
