#include <addons.h>

static
void Iter(ecs_iter_t *it) {
    Position *p = ecs_field(it, Position, 1);
    Position *p_parent = ecs_field(it, Position, 2);

    test_assert(!p_parent || !ecs_field_is_self(it, 2));

    probe_iter(it);

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

        if (p_parent) {
            p[i].x += p_parent->x;
            p[i].y += p_parent->y;
        }
    }
}

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

    ECS_COMPONENT(world, Position);

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

    ECS_SYSTEM(world, Iter, EcsOnUpdate, Position, ?Position(parent|cascade));
    ecs_system_init(world, &(ecs_system_desc_t){
        .entity = Iter,
        .query.filter.instanced = true
    });

    ecs_set(world, e1, Position, {1, 2});
    ecs_set(world, e2, Position, {1, 2});
    ecs_set(world, e3, Position, {1, 2});
    ecs_set(world, e4, Position, {1, 2});

    ecs_add_pair(world, e3, EcsChildOf, e1);
    ecs_add_pair(world, e4, EcsChildOf, e2);

    Probe ctx = {0};
    ecs_set_ctx(world, &ctx, NULL);

    ecs_progress(world, 1);

    test_int(ctx.count, 4);
    test_int(ctx.invoked, 3);
    test_int(ctx.system, Iter);
    test_int(ctx.term_count, 2);
    test_null(ctx.param);

    probe_has_entity(&ctx, e1);
    probe_has_entity(&ctx, e2);
    probe_has_entity(&ctx, e3);
    probe_has_entity(&ctx, e4);

    test_int(ctx.c[0][0], ecs_id(Position));
    test_int(ctx.s[0][0], 0);

    const Position *p = ecs_get(world, e1, Position);
    test_assert(p != NULL);
    test_int(p->x, 2);
    test_int(p->y, 3);

    p = ecs_get(world, e2, Position);
    test_assert(p != NULL);
    test_int(p->x, 2);
    test_int(p->y, 3);

    p = ecs_get(world, e3, Position);
    test_assert(p != NULL);
    test_int(p->x, 4);
    test_int(p->y, 6);

    p = ecs_get(world, e4, Position);
    test_assert(p != NULL);
    test_int(p->x, 4);
    test_int(p->y, 6);

    ecs_fini(world);
}

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

    ECS_COMPONENT(world, Position);

    ECS_ENTITY(world, e1, Position);
    ECS_ENTITY(world, e2, Position);
    ECS_ENTITY(world, e3, Position);
    ECS_ENTITY(world, e4, Position);
    ECS_ENTITY(world, e5, Position);
    ECS_ENTITY(world, e6, Position);

    ECS_SYSTEM(world, Iter, EcsOnUpdate, Position, ?Position(parent|cascade));
    ecs_system_init(world, &(ecs_system_desc_t){
        .entity = Iter,
        .query.filter.instanced = true
    });

    ecs_set(world, e1, Position, {1, 2});
    ecs_set(world, e2, Position, {1, 2});
    ecs_set(world, e3, Position, {1, 2});
    ecs_set(world, e4, Position, {1, 2});
    ecs_set(world, e5, Position, {1, 2});
    ecs_set(world, e6, Position, {1, 2});

    ecs_add_pair(world, e3, EcsChildOf, e1); /* depth 1 */
    ecs_add_pair(world, e4, EcsChildOf, e2); /* depth 1 */
    ecs_add_pair(world, e5, EcsChildOf, e3); /* depth 2 */
    ecs_add_pair(world, e6, EcsChildOf, e4); /* depth 2 */

    Probe ctx = {0};
    ecs_set_ctx(world, &ctx, NULL);

    ecs_progress(world, 1);

    test_int(ctx.count, 6);
    test_int(ctx.invoked, 5);
    test_int(ctx.system, Iter);
    test_int(ctx.term_count, 2);
    test_null(ctx.param);

    test_assert((ctx.e[0] == e1 && ctx.e[1] == e2) || (ctx.e[0] == e2 && ctx.e[1] == e1));
    test_assert((ctx.e[2] == e3 && ctx.e[3] == e4) || (ctx.e[2] == e4 && ctx.e[3] == e3));
    test_assert((ctx.e[4] == e5 && ctx.e[5] == e6) || (ctx.e[4] == e6 && ctx.e[5] == e5));

    test_int(ctx.c[0][0], ecs_id(Position));
    test_int(ctx.s[0][0], 0);

    const Position *p = ecs_get(world, e1, Position);
    test_assert(p != NULL);
    test_int(p->x, 2);
    test_int(p->y, 3);

    p = ecs_get(world, e2, Position);
    test_assert(p != NULL);
    test_int(p->x, 2);
    test_int(p->y, 3);

    p = ecs_get(world, e3, Position);
    test_assert(p != NULL);
    test_int(p->x, 4);
    test_int(p->y, 6);

    p = ecs_get(world, e4, Position);
    test_assert(p != NULL);
    test_int(p->x, 4);
    test_int(p->y, 6);

    p = ecs_get(world, e5, Position);
    test_assert(p != NULL);
    test_int(p->x, 6);
    test_int(p->y, 9);

    p = ecs_get(world, e6, Position);
    test_assert(p != NULL);
    test_int(p->x, 6);
    test_int(p->y, 9);

    ecs_fini(world);
}

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

    ECS_COMPONENT(world, Position);

    ECS_ENTITY(world, e1, Position);
    ECS_ENTITY(world, e2, Position);
    ECS_ENTITY(world, e3, Position);
    ECS_ENTITY(world, e4, Position);
    ECS_ENTITY(world, e5, Position);
    ECS_ENTITY(world, e6, Position);

    ECS_SYSTEM(world, Iter, EcsOnUpdate, Position, ?Position(cascade(ChildOf)));
    ecs_system_init(world, &(ecs_system_desc_t){
        .entity = Iter,
        .query.filter.instanced = true
    });

    ecs_set(world, e1, Position, {1, 2});
    ecs_set(world, e2, Position, {1, 2});
    ecs_set(world, e3, Position, {1, 2});
    ecs_set(world, e4, Position, {1, 2});
    ecs_set(world, e5, Position, {1, 2});
    ecs_set(world, e6, Position, {1, 2});

    ecs_add_pair(world, e3, EcsChildOf, e1); /* depth 1 */
    ecs_add_pair(world, e4, EcsChildOf, e2); /* depth 1 */
    ecs_add_pair(world, e5, EcsChildOf, e3); /* depth 2 */
    ecs_add_pair(world, e6, EcsChildOf, e4); /* depth 2 */

    Probe ctx = {0};
    ecs_set_ctx(world, &ctx, NULL);

    ecs_progress(world, 1);

    test_int(ctx.count, 6);
    test_int(ctx.invoked, 5);
    test_int(ctx.system, Iter);
    test_int(ctx.term_count, 2);
    test_null(ctx.param);

    test_assert((ctx.e[0] == e1 && ctx.e[1] == e2) || (ctx.e[0] == e2 && ctx.e[1] == e1));
    test_assert((ctx.e[2] == e3 && ctx.e[3] == e4) || (ctx.e[2] == e4 && ctx.e[3] == e3));
    test_assert((ctx.e[4] == e5 && ctx.e[5] == e6) || (ctx.e[4] == e6 && ctx.e[5] == e5));

    test_int(ctx.c[0][0], ecs_id(Position));
    test_int(ctx.s[0][0], 0);

    const Position *p = ecs_get(world, e1, Position);
    test_assert(p != NULL);
    test_int(p->x, 2);
    test_int(p->y, 3);

    p = ecs_get(world, e2, Position);
    test_assert(p != NULL);
    test_int(p->x, 2);
    test_int(p->y, 3);

    p = ecs_get(world, e3, Position);
    test_assert(p != NULL);
    test_int(p->x, 4);
    test_int(p->y, 6);

    p = ecs_get(world, e4, Position);
    test_assert(p != NULL);
    test_int(p->x, 4);
    test_int(p->y, 6);

    p = ecs_get(world, e5, Position);
    test_assert(p != NULL);
    test_int(p->x, 6);
    test_int(p->y, 9);

    p = ecs_get(world, e6, Position);
    test_assert(p != NULL);
    test_int(p->x, 6);
    test_int(p->y, 9);

    ecs_fini(world);
}

static
void AddParent(ecs_iter_t *it) {
    Position *p = ecs_field(it, Position, 1);
    Position *p_parent = ecs_field(it, Position, 2);

    test_assert(!p_parent || !ecs_field_is_self(it, 2));

    probe_iter(it);

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

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

    ECS_COMPONENT(world, Position);

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

    ECS_SYSTEM(world, AddParent, EcsOnUpdate, Position, ?Position(parent|cascade));
    ecs_system_init(world, &(ecs_system_desc_t){
        .entity = AddParent,
        .query.filter.instanced = true
    });

    ecs_entity_t parent = ecs_new(world, 0);
    ecs_set(world, e1, Position, {1, 2});
    ecs_set(world, e2, Position, {1, 2});
    ecs_set(world, e3, Position, {1, 2});
    ecs_set(world, e4, Position, {1, 2});

    ecs_add_pair(world, e3, EcsChildOf, parent); /* depth 1 */
    ecs_add_pair(world, e4, EcsChildOf, parent); /* depth 1 */

    Probe ctx = {0};
    ecs_set_ctx(world, &ctx, NULL);

    ecs_progress(world, 1);

    /* Before adding Position to parent, it wasn't being considered for the
     * column(parent|cascade), so tables could have been ordered randomly. Make sure
     * that queries can handle changes to depth after all tables are matched */
    ecs_set(world, parent, Position, {1, 2});

    ecs_os_zeromem(&ctx);

    ecs_progress(world, 1);

    test_int(ctx.count, 5);
    test_int(ctx.invoked, 3);
    test_int(ctx.system, AddParent);
    test_int(ctx.term_count, 2);
    test_null(ctx.param);

    test_assert(ctx.e[0] == e1 || ctx.e[1] == e1 || ctx.e[2] == e1);
    test_assert(ctx.e[0] == e2 || ctx.e[1] == e2 || ctx.e[2] == e2);
    test_assert(ctx.e[0] == parent || ctx.e[1] == parent || ctx.e[2] == parent);
    test_assert(ctx.e[3] == e3 || ctx.e[4] == e3);
    test_assert(ctx.e[3] == e4 || ctx.e[4] == e4);

    test_int(ctx.c[0][0], ecs_id(Position));
    test_int(ctx.s[0][0], 0);

    const Position *p = ecs_get(world, e1, Position);
    test_assert(p != NULL);
    test_int(p->x, 1);
    test_int(p->y, 2);

    p = ecs_get(world, e2, Position);
    test_assert(p != NULL);
    test_int(p->x, 1);
    test_int(p->y, 2);

    p = ecs_get(world, e3, Position);
    test_assert(p != NULL);
    test_int(p->x, 2);
    test_int(p->y, 4);

    p = ecs_get(world, e4, Position);
    test_assert(p != NULL);
    test_int(p->x, 2);
    test_int(p->y, 4);

    ecs_fini(world);
}

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

    ECS_COMPONENT(world, Position);

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

    ECS_SYSTEM(world, AddParent, EcsOnUpdate, Position, ?Position(parent|cascade));
    ecs_system_init(world, &(ecs_system_desc_t){
        .entity = AddParent,
        .query.filter.instanced = true
    });

    ecs_entity_t parent = ecs_set(world, 0, Position, {1, 2});
    ecs_set(world, e1, Position, {1, 2});
    ecs_set(world, e2, Position, {1, 2});
    ecs_set(world, e3, Position, {1, 2});
    ecs_set(world, e4, Position, {1, 2});

    Probe ctx = {0};
    ecs_set_ctx(world, &ctx, NULL);

    ecs_progress(world, 1);

    ecs_add_pair(world, e3, EcsChildOf, parent);
    ecs_add_pair(world, e4, EcsChildOf, parent);

    ecs_os_zeromem(&ctx);

    ecs_progress(world, 1);

    test_int(ctx.count, 5);
    test_int(ctx.invoked, 3);
    test_int(ctx.system, AddParent);
    test_int(ctx.term_count, 2);
    test_null(ctx.param);

    test_int(ctx.e[0], e1);
    test_int(ctx.e[1], e2);
    test_int(ctx.e[2], parent);
    test_int(ctx.e[3], e3);
    test_int(ctx.e[4], e4);
    test_int(ctx.c[0][0], ecs_id(Position));
    test_int(ctx.s[0][0], 0);

    const Position *p = ecs_get(world, e1, Position);
    test_assert(p != NULL);
    test_int(p->x, 1);
    test_int(p->y, 2);

    p = ecs_get(world, e2, Position);
    test_assert(p != NULL);
    test_int(p->x, 1);
    test_int(p->y, 2);

    p = ecs_get(world, e3, Position);
    test_assert(p != NULL);
    test_int(p->x, 2);
    test_int(p->y, 4);

    p = ecs_get(world, e4, Position);
    test_assert(p != NULL);
    test_int(p->x, 2);
    test_int(p->y, 4);

    ecs_fini(world);
}

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

    ECS_COMPONENT(world, Position);
    ECS_ENTITY(world, Rel, Traversable);

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

    ECS_SYSTEM(world, Iter, EcsOnUpdate, Position, ?Position(cascade(Rel)));
    ecs_system_init(world, &(ecs_system_desc_t){
        .entity = Iter,
        .query.filter.instanced = true
    });

    ecs_set(world, e1, Position, {1, 2});
    ecs_set(world, e2, Position, {1, 2});
    ecs_set(world, e3, Position, {1, 2});
    ecs_set(world, e4, Position, {1, 2});

    ecs_add_pair(world, e3, Rel, e1);
    ecs_add_pair(world, e4, Rel, e2);

    Probe ctx = {0};
    ecs_set_ctx(world, &ctx, NULL);

    ecs_progress(world, 1);

    test_int(ctx.count, 4);
    test_int(ctx.invoked, 3);
    test_int(ctx.system, Iter);
    test_int(ctx.term_count, 2);
    test_null(ctx.param);

    probe_has_entity(&ctx, e1);
    probe_has_entity(&ctx, e2);
    probe_has_entity(&ctx, e3);
    probe_has_entity(&ctx, e4);

    test_int(ctx.c[0][0], ecs_id(Position));
    test_int(ctx.s[0][0], 0);

    const Position *p = ecs_get(world, e1, Position);
    test_assert(p != NULL);
    test_int(p->x, 2);
    test_int(p->y, 3);

    p = ecs_get(world, e2, Position);
    test_assert(p != NULL);
    test_int(p->x, 2);
    test_int(p->y, 3);

    p = ecs_get(world, e3, Position);
    test_assert(p != NULL);
    test_int(p->x, 4);
    test_int(p->y, 6);

    p = ecs_get(world, e4, Position);
    test_assert(p != NULL);
    test_int(p->x, 4);
    test_int(p->y, 6);

    ecs_fini(world);
}

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

    ECS_COMPONENT(world, Position);
    ECS_ENTITY(world, Rel, Traversable);

    ECS_ENTITY(world, e1, Position);
    ECS_ENTITY(world, e2, Position);
    ECS_ENTITY(world, e3, Position);
    ECS_ENTITY(world, e4, Position);
    ECS_ENTITY(world, e5, Position);
    ECS_ENTITY(world, e6, Position);

    ECS_SYSTEM(world, Iter, EcsOnUpdate, Position, ?Position(cascade(Rel)));
    ecs_system_init(world, &(ecs_system_desc_t){
        .entity = Iter,
        .query.filter.instanced = true
    });

    ecs_set(world, e1, Position, {1, 2});
    ecs_set(world, e2, Position, {1, 2});
    ecs_set(world, e3, Position, {1, 2});
    ecs_set(world, e4, Position, {1, 2});
    ecs_set(world, e5, Position, {1, 2});
    ecs_set(world, e6, Position, {1, 2});

    ecs_add_pair(world, e3, Rel, e1); /* depth 1 */
    ecs_add_pair(world, e4, Rel, e2); /* depth 1 */
    ecs_add_pair(world, e5, Rel, e3); /* depth 2 */
    ecs_add_pair(world, e6, Rel, e4); /* depth 2 */

    Probe ctx = {0};
    ecs_set_ctx(world, &ctx, NULL);

    ecs_progress(world, 1);

    test_int(ctx.count, 6);
    test_int(ctx.invoked, 5);
    test_int(ctx.system, Iter);
    test_int(ctx.term_count, 2);
    test_null(ctx.param);

    test_assert((ctx.e[0] == e1 && ctx.e[1] == e2) || (ctx.e[0] == e2 && ctx.e[1] == e1));
    test_assert((ctx.e[2] == e3 && ctx.e[3] == e4) || (ctx.e[2] == e4 && ctx.e[3] == e3));
    test_assert((ctx.e[4] == e5 && ctx.e[5] == e6) || (ctx.e[4] == e6 && ctx.e[5] == e5));

    test_int(ctx.c[0][0], ecs_id(Position));
    test_int(ctx.s[0][0], 0);

    const Position *p = ecs_get(world, e1, Position);
    test_assert(p != NULL);
    test_int(p->x, 2);
    test_int(p->y, 3);

    p = ecs_get(world, e2, Position);
    test_assert(p != NULL);
    test_int(p->x, 2);
    test_int(p->y, 3);

    p = ecs_get(world, e3, Position);
    test_assert(p != NULL);
    test_int(p->x, 4);
    test_int(p->y, 6);

    p = ecs_get(world, e4, Position);
    test_assert(p != NULL);
    test_int(p->x, 4);
    test_int(p->y, 6);

    p = ecs_get(world, e5, Position);
    test_assert(p != NULL);
    test_int(p->x, 6);
    test_int(p->y, 9);

    p = ecs_get(world, e6, Position);
    test_assert(p != NULL);
    test_int(p->x, 6);
    test_int(p->y, 9);

    ecs_fini(world);
}

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

    ECS_COMPONENT(world, Position);
    ECS_ENTITY(world, Rel, Traversable);

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

    ECS_SYSTEM(world, AddParent, EcsOnUpdate, Position, ?Position(cascade(Rel)));
    ecs_system_init(world, &(ecs_system_desc_t){
        .entity = AddParent,
        .query.filter.instanced = true
    });

    ecs_entity_t parent = ecs_new(world, 0);
    ecs_set(world, e1, Position, {1, 2});
    ecs_set(world, e2, Position, {1, 2});
    ecs_set(world, e3, Position, {1, 2});
    ecs_set(world, e4, Position, {1, 2});

    ecs_add_pair(world, e3, Rel, parent); /* depth 1 */
    ecs_add_pair(world, e4, Rel, parent); /* depth 1 */

    Probe ctx = {0};
    ecs_set_ctx(world, &ctx, NULL);

    ecs_progress(world, 1);

    /* Before adding Position to parent, it wasn't being considered for the
     * column(parent|cascade), so tables could have been ordered randomly. Make sure
     * that queries can handle changes to depth after all tables are matched */
    ecs_set(world, parent, Position, {1, 2});

    ecs_os_zeromem(&ctx);

    ecs_progress(world, 1);

    test_int(ctx.count, 5);
    test_int(ctx.invoked, 3);
    test_int(ctx.system, AddParent);
    test_int(ctx.term_count, 2);
    test_null(ctx.param);

    test_assert(ctx.e[0] == e1 || ctx.e[1] == e1 || ctx.e[2] == e1);
    test_assert(ctx.e[0] == e2 || ctx.e[1] == e2 || ctx.e[2] == e2);
    test_assert(ctx.e[0] == parent || ctx.e[1] == parent || ctx.e[2] == parent);
    test_assert(ctx.e[3] == e3 || ctx.e[4] == e3);
    test_assert(ctx.e[3] == e4 || ctx.e[4] == e4);

    test_int(ctx.c[0][0], ecs_id(Position));
    test_int(ctx.s[0][0], 0);

    const Position *p = ecs_get(world, e1, Position);
    test_assert(p != NULL);
    test_int(p->x, 1);
    test_int(p->y, 2);

    p = ecs_get(world, e2, Position);
    test_assert(p != NULL);
    test_int(p->x, 1);
    test_int(p->y, 2);

    p = ecs_get(world, e3, Position);
    test_assert(p != NULL);
    test_int(p->x, 2);
    test_int(p->y, 4);

    p = ecs_get(world, e4, Position);
    test_assert(p != NULL);
    test_int(p->x, 2);
    test_int(p->y, 4);

    ecs_fini(world);
}

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

    ECS_COMPONENT(world, Position);
    ECS_ENTITY(world, Rel, Traversable);

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

    ECS_SYSTEM(world, AddParent, EcsOnUpdate, Position, ?Position(cascade(Rel)));
    ecs_system_init(world, &(ecs_system_desc_t){
        .entity = AddParent,
        .query.filter.instanced = true
    });

    ecs_entity_t parent = ecs_set(world, 0, Position, {1, 2});
    ecs_set(world, e1, Position, {1, 2});
    ecs_set(world, e2, Position, {1, 2});
    ecs_set(world, e3, Position, {1, 2});
    ecs_set(world, e4, Position, {1, 2});

    Probe ctx = {0};
    ecs_set_ctx(world, &ctx, NULL);

    ecs_progress(world, 1);

    ecs_add_pair(world, e3, Rel, parent);
    ecs_add_pair(world, e4, Rel, parent);

    ecs_os_zeromem(&ctx);

    ecs_progress(world, 1);

    test_int(ctx.count, 5);
    test_int(ctx.invoked, 3);
    test_int(ctx.system, AddParent);
    test_int(ctx.term_count, 2);
    test_null(ctx.param);

    test_int(ctx.e[0], e1);
    test_int(ctx.e[1], e2);
    test_int(ctx.e[2], parent);
    test_int(ctx.e[3], e3);
    test_int(ctx.e[4], e4);
    test_int(ctx.c[0][0], ecs_id(Position));
    test_int(ctx.s[0][0], 0);

    const Position *p = ecs_get(world, e1, Position);
    test_assert(p != NULL);
    test_int(p->x, 1);
    test_int(p->y, 2);

    p = ecs_get(world, e2, Position);
    test_assert(p != NULL);
    test_int(p->x, 1);
    test_int(p->y, 2);

    p = ecs_get(world, e3, Position);
    test_assert(p != NULL);
    test_int(p->x, 2);
    test_int(p->y, 4);

    p = ecs_get(world, e4, Position);
    test_assert(p != NULL);
    test_int(p->x, 2);
    test_int(p->y, 4);

    ecs_fini(world);
}
