#include <api.h>

static
void Trigger(ecs_iter_t *it) {
    probe_iter(it);
}

static
void OnSet(ecs_iter_t *it) {
    Position *p = ecs_field(it, Position, 1);

    Velocity *v = NULL;
    if (it->field_count >= 2) {
        v = ecs_field(it, Velocity, 2);
    }

    probe_iter(it);

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

static bool dummy_called = false;

static
void Dummy(ecs_iter_t *it) {
    dummy_called = true;
}

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

    ECS_COMPONENT(world, Position);

    ECS_OBSERVER(world, OnSet, EcsOnSet, Position);

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

    ecs_entity_t e = ecs_new(world, Position);
    test_int(ctx.count, 0);

    ecs_set(world, e, Position, {10, 20});
    
    test_int(ctx.count, 1);
    test_int(ctx.invoked, 1);
    test_int(ctx.system, OnSet);
    test_int(ctx.term_count, 1);
    test_null(ctx.param);

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

    const Position *p = ecs_get(world, e, Position);
    test_assert(p != NULL);
    test_int(p->x, 11);
    test_int(p->y, 20);

    ecs_fini(world);
}

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

    ECS_COMPONENT(world, Position);

    ECS_OBSERVER(world, OnSet, EcsOnSet, Position);

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

    ecs_entity_t e = ecs_set(world, 0, Position, {10, 20});
    
    test_int(ctx.count, 1);
    test_int(ctx.invoked, 1);
    test_int(ctx.system, OnSet);
    test_int(ctx.term_count, 1);
    test_null(ctx.param);

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

    const Position *p = ecs_get(world, e, Position);
    test_assert(p != NULL);
    test_int(p->x, 11);
    test_int(p->y, 20);

    ecs_fini(world);
}

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

    ECS_COMPONENT(world, Position);

    ECS_OBSERVER(world, OnSet, EcsOnSet, Position);

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

    ecs_entity_t e = ecs_new(world, Position);
    test_int(ctx.count, 0);

    ecs_set(world, e, Position, {10, 20});
    
    test_int(ctx.count, 1);
    test_int(ctx.invoked, 1);
    test_int(ctx.system, OnSet);
    test_int(ctx.term_count, 1);
    test_null(ctx.param);

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

    const Position *p = ecs_get(world, e, Position);
    test_assert(p != NULL);
    test_int(p->x, 11);
    test_int(p->y, 20);

    ecs_set_ptr(world, e, Position, p);

    p = ecs_get(world, e, Position);
    test_assert(p != NULL);
    test_int(p->x, 12);
    test_int(p->y, 20);

    ecs_fini(world);
}

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

    ECS_COMPONENT(world, Position);

    ECS_OBSERVER(world, OnSet, EcsOnSet, Position);

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

    ecs_entity_t e1 = ecs_set(world, 0, Position, {10, 20});
    
    test_int(ctx.count, 1);
    test_int(ctx.invoked, 1);
    test_int(ctx.system, OnSet);
    test_int(ctx.term_count, 1);
    test_null(ctx.param);

    test_int(ctx.e[0], e1);
    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, 11);
    test_int(p->y, 20);

    ecs_os_zeromem(&ctx);

   ecs_clone(world, 0, e1, false);

    /* OnSet function should not have been called, because value has not been 
     * copied */
    test_int(ctx.count, 0);

    ecs_fini(world);
}

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

    ECS_COMPONENT(world, Position);

    ECS_OBSERVER(world, OnSet, EcsOnSet, Position);

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

    ecs_entity_t e1 = ecs_set(world, 0, Position, {10, 20});
    
    test_int(ctx.count, 1);
    test_int(ctx.invoked, 1);
    test_int(ctx.system, OnSet);
    test_int(ctx.term_count, 1);
    test_null(ctx.param);

    test_int(ctx.e[0], e1);
    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, 11);
    test_int(p->y, 20);

    ecs_os_zeromem(&ctx);

    ecs_entity_t e2 = ecs_clone(world, 0, e1, true);

    test_int(ctx.count, 1);
    test_int(ctx.invoked, 1);
    test_int(ctx.system, OnSet);
    test_int(ctx.term_count, 1);
    test_null(ctx.param);

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

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

    ecs_fini(world);
}

static bool add_called;
static bool set_called;

static
void OnAdd_check_order(ecs_iter_t *it) {
    Position *p = ecs_field(it, Position, 1);

    test_assert(!add_called);
    test_assert(!set_called);

    probe_iter(it);

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

    add_called = true;
}

static
void OnSet_check_order(ecs_iter_t *it) {
    Position *p = ecs_field(it, Position, 1);

    probe_iter(it);

    test_assert(add_called);
    test_assert(!set_called);

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

    set_called = true;
}

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

    ECS_COMPONENT(world, Position);

    ECS_OBSERVER(world, OnSet_check_order, EcsOnSet, Position);
    ECS_OBSERVER(world, OnAdd_check_order, EcsOnAdd, Position);

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

    ecs_entity_t e = ecs_new(world, 0);
    test_int(ctx.count, 0);

    ecs_set(world, e, Position, {10, 20});
    
    test_int(ctx.count, 2);
    test_int(ctx.invoked, 2);
    test_int(ctx.system, OnSet_check_order);
    test_int(ctx.term_count, 1);
    test_null(ctx.param);

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

    const Position *p = ecs_get(world, e, Position);
    test_assert(p != NULL);
    test_int(p->x, 11);
    test_int(p->y, 20);

    test_assert(set_called);

    ecs_fini(world);
}

static
void OnSetShared(ecs_iter_t *it) {
    Position *p = ecs_field(it, Position, 1);

    Velocity *v = NULL;
    if (it->field_count >= 2) {
        v = ecs_field(it, Velocity, 2);
    }

    probe_iter(it);

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

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

    ECS_COMPONENT(world, Position);

    ECS_PREFAB(world, Prefab, Position);
    ecs_set(world, Prefab, Position, {1, 3});

    ECS_OBSERVER(world, OnSetShared, EcsOnSet, Position);

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

    /* instantiate prefab */
    ecs_entity_t e = ecs_new_w_pair(world, EcsIsA, Prefab);

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

    test_int(ctx.system, OnSetShared);
    test_int(ctx.term_count, 1);
    test_null(ctx.param);

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

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

    /* Override component. Should not call OnSet system because the value of the
     * component has not changed. */

    ecs_os_zeromem(&ctx);

    ecs_add(world, e, Position);

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

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

    /* Set component */

    ecs_set(world, e, Position, {2, 4});

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

    test_int(ctx.system, OnSetShared);
    test_int(ctx.term_count, 1);
    test_null(ctx.param);

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

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

    ecs_fini(world);
}

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

    ECS_COMPONENT(world, Position);

    ECS_PREFAB(world, Prefab, Position, OVERRIDE | Position);
    ecs_set(world, Prefab, Position, {1, 3});

    Probe ctx = {0};
    ecs_entity_t t1 = ecs_observer_init(world, &(ecs_observer_desc_t){
        .filter.terms[0].id = ecs_id(Position),
        .events = {EcsOnSet},
        .callback = OnSet,
        .ctx = &ctx
    });

    ecs_entity_t e = ecs_new_w_pair(world, EcsIsA, Prefab);

    test_int(ctx.count, 1);
    test_int(ctx.invoked, 1);
    test_int(ctx.system, t1);
    test_int(ctx.term_count, 1);
    test_null(ctx.param);

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

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

    ecs_fini(world);
}

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

    ECS_COMPONENT(world, Position);

    ECS_PREFAB(world, Prefab, Position, OVERRIDE | Position);
    ecs_set(world, Prefab, Position, {1, 3});

    ECS_OBSERVER(world, OnSet, EcsOnSet, Position);

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

    const ecs_entity_t *ids = ecs_bulk_new_w_id(world, ecs_pair(EcsIsA, Prefab), 3);
    test_assert(ids != NULL);

    test_int(ctx.count, 3);
    test_int(ctx.invoked, 1);
    test_int(ctx.system, OnSet);
    test_int(ctx.term_count, 1);
    test_null(ctx.param);

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

    int i;
    for (i = 0; i < 3; i ++) {
        const Position *p = ecs_get(world, ids[i], Position);
        test_assert(p != NULL);
        test_int(p->x, 2);
        test_int(p->y, 3);
    }

    ecs_fini(world);
}

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

    ECS_COMPONENT(world, Position);

    ECS_PREFAB(world, Prefab, Position, OVERRIDE | Position);
    ecs_set(world, Prefab, Position, {1, 3});

    ECS_OBSERVER(world, OnSet, EcsOnSet, Position);

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

    ecs_entity_t e = ecs_new_w_pair(world, EcsIsA, Prefab);
    test_int(ctx.count, 1);
    test_int(ctx.invoked, 1);
    test_int(ctx.system, OnSet);
    test_int(ctx.term_count, 1);
    test_null(ctx.param);

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

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

static
void SetPosition(ecs_iter_t *it) {
    probe_iter(it);
}

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

    ECS_COMPONENT(world, Position);
    ECS_OBSERVER(world, SetPosition, EcsOnSet, Position);

    const ecs_entity_t *ids = ecs_bulk_new(world, Position, 10);
    test_assert(ids != NULL);

    ecs_entity_t id_arr[10];
    memcpy(id_arr, ids, sizeof(ecs_entity_t) * 10);

    int32_t i;
    for (i = 0; i < 10; i ++) {
        test_assert(ecs_has(world, ids[i], Position));
        ecs_set(world, ids[i], Position, {i, i * 2});
    }

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

    ecs_snapshot_t *s = ecs_snapshot_take(world);

    test_int(ctx.invoked, 0);

    /* Delete one entity, so we have more confidence we're triggering on the 
     * right entities */
    ecs_delete(world, id_arr[0]);
    
    test_int(ctx.invoked, 0);

    ecs_snapshot_restore(world, s);

    test_int(ctx.count, 10);
    test_int(ctx.invoked, 1);
    test_int(ctx.system, SetPosition);
    test_int(ctx.term_count, 1);
    test_int(ctx.c[0][0], ecs_id(Position));
    test_null(ctx.param);
    
    for (i = 0; i < 10; i ++) {
        test_int(ctx.e[i], id_arr[i]);
    }

    ecs_fini(world);
}

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

    ECS_COMPONENT(world, Position);

    ecs_observer_init(world, &(ecs_observer_desc_t){
        .filter.terms[0].id = ecs_id(Position),
        .events = {EcsOnSet},
        .callback = Dummy
    });

    ecs_entity_t e = ecs_new_id(world);
    test_assert(e != 0);
    test_int(dummy_called, 0);

    Position *p = ecs_emplace(world, e, Position);
    test_assert(p != NULL);
    test_int(dummy_called, 0);

    ecs_modified(world, e, Position);
    test_bool(dummy_called, true);

    ecs_fini(world);
}

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

    ECS_TAG(world, Tag);

    ecs_observer_init(world, &(ecs_observer_desc_t){
        .filter.terms[0].id = Tag,
        .events = {EcsUnSet},
        .callback = Dummy
    });

    ecs_entity_t e = ecs_new_id(world);
    test_assert(e != 0);
    test_int(dummy_called, 0);

    ecs_add(world, e, Tag);
    test_int(dummy_called, 0);

    ecs_remove(world, e, Tag);
    test_int(dummy_called, 1);

    ecs_fini(world);
}

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

    ECS_TAG(world, Tag);

    ecs_observer_init(world, &(ecs_observer_desc_t){
        .filter.terms[0].id = Tag,
        .events = {EcsUnSet},
        .callback = Dummy
    });

    ecs_entity_t e = ecs_new_id(world);
    test_assert(e != 0);
    test_int(dummy_called, 0);

    ecs_add(world, e, Tag);
    test_int(dummy_called, 0);

    ecs_clear(world, e);
    test_int(dummy_called, 1);

    ecs_fini(world);
}

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

    ECS_TAG(world, Tag);

    ecs_observer_init(world, &(ecs_observer_desc_t){
        .filter.terms[0].id = Tag,
        .events = {EcsUnSet},
        .callback = Dummy
    });

    ecs_entity_t e = ecs_new_id(world);
    test_assert(e != 0);
    test_int(dummy_called, 0);

    ecs_add(world, e, Tag);
    test_int(dummy_called, 0);

    ecs_delete(world, e);
    test_int(dummy_called, 1);

    ecs_fini(world);
}

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

    ECS_COMPONENT(world, Position);
    ECS_COMPONENT(world, Velocity);
    ECS_ENTITY(world, Base, Position);
    ECS_OBSERVER(world, Trigger, EcsOnSet, Position);

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

    ecs_entity_t e = ecs_new(world, Position);
    test_int(ctx.invoked, 0);

    ecs_add_pair(world, e, EcsIsA, Base);
    test_int(ctx.invoked, 0);

    ecs_remove(world, e, Position);
    test_int(ctx.invoked, 1);
    test_int(ctx.count, 1);
    test_int(ctx.system, Trigger);
    test_int(ctx.term_count, 1);
    test_null(ctx.param);

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

    ecs_fini(world);
}
