/**
 * @file entity_name.c
 * @brief Functions for working with named entities.
 */

#include "private_api.h"
#include <ctype.h>

#define ECS_NAME_BUFFER_LENGTH (64)

static
bool flecs_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_assert(sep[0] != 0, ECS_INVALID_PARAMETER, NULL);

    ecs_entity_t cur = 0;
    const char *name = NULL;
    ecs_size_t name_len = 0;

    if (child && ecs_is_alive(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)) {
                flecs_path_append(world, parent, cur, sep, prefix, buf);
                if (!sep[1]) {
                    ecs_strbuf_appendch(buf, sep[0]);
                } else {
                    ecs_strbuf_appendstr(buf, sep);
                }
            }
        } else if (prefix && prefix[0]) {
            if (!prefix[1]) {
                ecs_strbuf_appendch(buf, prefix[0]);
            } else {
                ecs_strbuf_appendstr(buf, prefix);
            }
        }

        const EcsIdentifier *id = ecs_get_pair(
            world, child, EcsIdentifier, EcsName);
        if (id) {
            name = id->value;
            name_len = id->length;
        }      
    }

    if (name) {
        ecs_strbuf_appendstrn(buf, name, name_len);
    } else {
        ecs_strbuf_appendint(buf, flecs_uto(int64_t, (uint32_t)child));
    }

    return cur != 0;
}

bool flecs_name_is_id(
    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;
}

ecs_entity_t flecs_name_to_id(
    const ecs_world_t *world,
    const char *name)
{
    int64_t result = atoll(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 {
        if ((uint32_t)result == (uint64_t)result) {
            return (ecs_entity_t)result;
        } else {
            return 0;
        }
    }
}

static
ecs_entity_t flecs_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;
    } else if (name[0] == '$' && name[1] == '\0') {
        return EcsVariable;
    }

    return 0;
}

static
bool flecs_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* flecs_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 && flecs_is_sep(&ptr, sep)) {
            break;
        }

        count ++;
    }

    if (len) {
        *len = count;
    }

    if (count) {
        return ptr;
    } else {
        return NULL;
    }
error:
    return NULL;
}

static
bool flecs_is_root_path(
    const char *path,
    const char *prefix)
{
    if (prefix) {
        return !ecs_os_strncmp(path, prefix, ecs_os_strlen(prefix));
    } else {
        return false;
    }
}

static
ecs_entity_t flecs_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 (flecs_is_root_path(path, prefix)) {
        path += ecs_os_strlen(prefix);
        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 flecs_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(world, {
        .entity = ecs_entity(world, {.add = {ecs_childof(EcsFlecsInternals)}}),
        .filter.terms[0] = {
            .id = ecs_pair(ecs_id(EcsIdentifier), EcsSymbol), 
            .src.flags = EcsSelf 
        },
        .callback = flecs_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_appendch(buf, '*');
        return;
    }
    if (child == EcsAny) {
        ecs_strbuf_appendch(buf, '_');
        return;
    }

    if (!sep) {
        sep = ".";
    }

    if (!child || parent != child) {
        flecs_path_append(world, parent, child, sep, prefix, buf);
    } else {
        ecs_strbuf_appendstrn(buf, "", 0);
    }

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 (flecs_name_is_id(name)) {
        ecs_entity_t result = flecs_name_to_id(world, name);
        if (result && ecs_is_alive(world, result)) {
            if (parent && !ecs_has_pair(world, result, EcsChildOf, parent)) {
                return 0;
            }
            return result;
        }
    }

    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 = flecs_get_builtin(name);
    if (e) {
        return e;
    }

    if (flecs_name_is_id(name)) {
        return flecs_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,
    bool recursive)
{   
    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_path_w_sep(world, 0, name, ".", NULL, recursive);
    }

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

    ecs_check(world != NULL, ECS_INTERNAL_ERROR, NULL);
    const ecs_world_t *stage = world;
    world = ecs_get_world(world);

    ecs_entity_t e = flecs_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;

    const ecs_entity_t *lookup_path = ecs_get_lookup_path(stage);
    const ecs_entity_t *lookup_path_cur = lookup_path;
    while (lookup_path_cur && *lookup_path_cur) {
        lookup_path_cur ++;
    }

    if (!sep) {
        sep = ".";
    }

    parent = flecs_get_parent_from_path(stage, parent, &path, prefix, true);

    if (!sep[0]) {
        return ecs_lookup_child(world, parent, path);
    }

retry:
    cur = parent;
    ptr_start = ptr = path;

    while ((ptr = flecs_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);

    /* Safe: application owns lookup path */
    ecs_entity_t *cur = ECS_CONST_CAST(ecs_entity_t*, stage->lookup_path);
    stage->lookup_path = 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);
    /* Safe: application owns lookup path */
    return ECS_CONST_CAST(ecs_entity_t*, 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->info.name_prefix;
    world->info.name_prefix = prefix;
    return old_prefix;
}

static
void flecs_add_path(
    ecs_world_t *world,
    bool defer_suspend,
    ecs_entity_t parent,
    ecs_entity_t entity,
    const char *name)
{
    ecs_suspend_readonly_state_t srs;
    ecs_world_t *real_world = NULL;
    if (defer_suspend) {
        real_world = flecs_suspend_readonly(world, &srs);
        ecs_assert(real_world != NULL, ECS_INTERNAL_ERROR, NULL);
    }

    if (parent) {
        ecs_add_pair(world, entity, EcsChildOf, parent);
    }

    ecs_set_name(world, entity, name);

    if (defer_suspend) {
        flecs_resume_readonly(real_world, &srs);
        flecs_defer_path((ecs_stage_t*)world, parent, entity, name);
    }
}

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

    bool root_path = flecs_is_root_path(path, prefix);
    parent = flecs_get_parent_from_path(world, parent, &path, prefix, !entity);

    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;
    
    /* If we're in deferred/readonly mode suspend it, so that the name index is
     * immediately updated. Without this, we could create multiple entities for
     * the same name in a single command queue. */
    bool suspend_defer = ecs_poly_is(world, ecs_stage_t) && 
        (ecs_get_stage_count(world) <= 1);
    ecs_entity_t cur = parent;
    char *name = NULL;

    if (sep[0]) {
        while ((ptr = flecs_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 (!flecs_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 && last_elem && root_path) {
                    ecs_remove_pair(world, e, EcsChildOf, EcsWildcard);
                }

                flecs_add_path(world, suspend_defer, cur, 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);
        }
    } else {
        flecs_add_path(world, suspend_defer, parent, entity, path);
    }

    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)
{
    return ecs_add_path_w_sep(world, 0, parent, path, sep, prefix);
}
