
/*
 * Copyright (C) Igor Sysoev
 * Copyright (C) Dmitry Volyntsev
 * Copyright (C) NGINX, Inc.
 */


#include <njs_main.h>


static njs_declaration_t *njs_variable_scope_function_add(njs_parser_t *parser,
    njs_parser_scope_t *scope);
static njs_parser_scope_t *njs_variable_scope_find(njs_parser_t *parser,
     njs_parser_scope_t *scope, uintptr_t atom_id, njs_variable_type_t type);
static njs_variable_t *njs_variable_alloc(njs_vm_t *vm, uintptr_t atom_id,
    njs_variable_type_t type);


njs_variable_t *
njs_variable_add(njs_parser_t *parser, njs_parser_scope_t *scope,
    uintptr_t atom_id, njs_variable_type_t type)
{
    njs_parser_scope_t  *root;

    root = njs_variable_scope_find(parser, scope, atom_id, type);
    if (njs_slow_path(root == NULL)) {
        njs_parser_ref_error(parser, "scope not found");
        return NULL;
    }

    return njs_variable_scope_add(parser, root, scope, atom_id, type,
                                  NJS_INDEX_NONE);
}


njs_variable_t *
njs_variable_function_add(njs_parser_t *parser, njs_parser_scope_t *scope,
    uintptr_t atom_id)
{
    njs_bool_t             ctor;
    njs_variable_t         *var;
    njs_declaration_t      *declr;
    njs_parser_scope_t     *root;
    njs_function_lambda_t  *lambda;

    root = njs_variable_scope_find(parser, scope, atom_id,
                                   NJS_VARIABLE_FUNCTION);
    if (njs_slow_path(root == NULL)) {
        njs_parser_ref_error(parser, "scope not found");
        return NULL;
    }

    var = njs_variable_scope_add(parser, root, scope, atom_id,
                                 NJS_VARIABLE_FUNCTION, NJS_INDEX_ERROR);
    if (njs_slow_path(var == NULL)) {
        return NULL;
    }

    root = njs_function_scope(scope);
    if (njs_slow_path(scope == NULL)) {
        return NULL;
    }

    ctor = parser->node->token_type != NJS_TOKEN_ASYNC_FUNCTION_DECLARATION;

    lambda = njs_function_lambda_alloc(parser->vm, ctor);
    if (lambda == NULL) {
        return NULL;
    }

    njs_set_invalid(&var->value);
    var->value.data.u.lambda = lambda;

    declr = njs_variable_scope_function_add(parser, root);
    if (njs_slow_path(declr == NULL)) {
        return NULL;
    }

    var->index = njs_scope_index(root->type, root->items, NJS_LEVEL_LOCAL,
                                 NJS_VARIABLE_FUNCTION);

    declr->lambda = lambda;
    declr->async = !ctor;
    declr->index = var->index;

    root->items++;

    var->type = NJS_VARIABLE_FUNCTION;

    return var;
}


static njs_declaration_t *
njs_variable_scope_function_add(njs_parser_t *parser, njs_parser_scope_t *scope)
{
    if (scope->declarations == NULL) {
        scope->declarations = njs_arr_create(parser->vm->mem_pool, 1,
                                             sizeof(njs_declaration_t));
        if (njs_slow_path(scope->declarations == NULL)) {
            return NULL;
        }
    }

    return njs_arr_add(scope->declarations);
}


static njs_parser_scope_t *
njs_variable_scope(njs_parser_scope_t *scope, uintptr_t atom_id,
    njs_variable_t **retvar, njs_variable_type_t type)
{
    njs_variable_t       *var;
    njs_rbtree_node_t    *node;
    njs_variable_node_t  var_node;

    *retvar = NULL;

    var_node.key = atom_id;

    do {
        node = njs_rbtree_find(&scope->variables, &var_node.node);

        if (node != NULL) {
            var = ((njs_variable_node_t *) node)->variable;

            if (var->type != NJS_VARIABLE_CATCH || type != NJS_VARIABLE_VAR) {
                *retvar = var;
                return scope;
            }
        }

        if (scope->type == NJS_SCOPE_GLOBAL
            || scope->type == NJS_SCOPE_FUNCTION)
        {
            return scope;
        }

        scope = scope->parent;

    } while (scope != NULL);

    return NULL;
}


static njs_parser_scope_t *
njs_variable_scope_find(njs_parser_t *parser, njs_parser_scope_t *scope,
     uintptr_t atom_id, njs_variable_type_t type)
{
    njs_str_t           entry;
    njs_bool_t          module;
    njs_variable_t      *var;
    njs_parser_scope_t  *root;

    root = njs_variable_scope(scope, atom_id, &var, type);
    if (njs_slow_path(root == NULL)) {
        return NULL;
    }

    switch (type) {
    case NJS_VARIABLE_CONST:
    case NJS_VARIABLE_LET:
        if (scope->type == NJS_SCOPE_GLOBAL
            && atom_id == NJS_ATOM_STRING_undefined)
        {
            goto failed;
        }

        if (root != scope) {
            return scope;
        }

        if (var != NULL && var->scope == root) {
            if (var->self) {
                return scope;
            }

            goto failed;
        }

        return scope;

    case NJS_VARIABLE_VAR:
    case NJS_VARIABLE_FUNCTION:
        break;

    default:
        return scope;
    }

    if (type == NJS_VARIABLE_FUNCTION) {
        root = scope;
    }

    if (var == NULL) {
        return root;
    }

    if (var->type == NJS_VARIABLE_LET || var->type == NJS_VARIABLE_CONST) {
        goto failed;
    }

    if (var->original->type == NJS_SCOPE_BLOCK) {
        if (type == NJS_VARIABLE_FUNCTION
            || var->type == NJS_VARIABLE_FUNCTION)
        {
            if (var->original == root) {
                goto failed;
            }
        }
    }

    if (type != NJS_VARIABLE_FUNCTION
        && var->type != NJS_VARIABLE_FUNCTION)
    {
        return var->scope;
    }

    if (root != scope) {
        return root;
    }

    if (scope->parent == NULL) {
        module = parser->vm->options.module || parser->module;

        if (module) {
            if (type == NJS_VARIABLE_FUNCTION
                || var->type == NJS_VARIABLE_FUNCTION)
            {
                goto failed;
            }
        }

    }

    return root;

failed:

    njs_atom_string_get(parser->vm, atom_id, &entry);

    njs_parser_syntax_error(parser, "\"%V\" has already been declared", &entry);
    return NULL;
}


njs_variable_t *
njs_variable_scope_add(njs_parser_t *parser, njs_parser_scope_t *scope,
    njs_parser_scope_t *original, uintptr_t atom_id,
    njs_variable_type_t type, njs_index_t index)
{
    njs_variable_t       *var;
    njs_rbtree_node_t    *node;
    njs_parser_scope_t   *root;
    njs_variable_node_t  var_node, *var_node_new;

    var_node.key = atom_id;

    node = njs_rbtree_find(&scope->variables, &var_node.node);

    if (node != NULL) {
        return ((njs_variable_node_t *) node)->variable;
    }

    var = njs_variable_alloc(parser->vm, atom_id, type);
    if (njs_slow_path(var == NULL)) {
        goto memory_error;
    }

    var->scope = scope;
    var->index = index;
    var->original = original;

    if (index == NJS_INDEX_NONE) {
        root = njs_function_scope(scope);
        if (njs_slow_path(scope == NULL)) {
            return NULL;
        }

        var->index = njs_scope_index(root->type, root->items, NJS_LEVEL_LOCAL,
                                     type);
        root->items++;
    }

    var_node_new = njs_variable_node_alloc(parser->vm, var, atom_id);
    if (njs_slow_path(var_node_new == NULL)) {
        goto memory_error;
    }

    njs_rbtree_insert(&scope->variables, &var_node_new->node);

    return var;

memory_error:

    njs_memory_error(parser->vm);

    return NULL;
}


njs_variable_t *
njs_label_add(njs_vm_t *vm, njs_parser_scope_t *scope, uintptr_t atom_id)
{
    njs_variable_t       *label;
    njs_rbtree_node_t    *node;
    njs_variable_node_t  var_node, *var_node_new;

    var_node.key = atom_id;

    node = njs_rbtree_find(&scope->labels, &var_node.node);

    if (node != NULL) {
        return ((njs_variable_node_t *) node)->variable;
    }

    label = njs_variable_alloc(vm, atom_id, NJS_VARIABLE_CONST);
    if (njs_slow_path(label == NULL)) {
        goto memory_error;
    }

    var_node_new = njs_variable_node_alloc(vm, label, atom_id);
    if (njs_slow_path(var_node_new == NULL)) {
        goto memory_error;
    }

    njs_rbtree_insert(&scope->labels, &var_node_new->node);

    return label;

memory_error:

    njs_memory_error(vm);

    return NULL;
}


njs_int_t
njs_label_remove(njs_vm_t *vm, njs_parser_scope_t *scope, uintptr_t atom_id)
{
    njs_rbtree_node_t    *node;
    njs_variable_node_t  var_node;

    var_node.key = atom_id;

    node = njs_rbtree_find(&scope->labels, &var_node.node);
    if (njs_slow_path(node == NULL)) {
        njs_internal_error(vm, "failed to find label while removing");
        return NJS_ERROR;
    }

    njs_rbtree_delete(&scope->labels, (njs_rbtree_part_t *) node);
    njs_variable_node_free(vm, (njs_variable_node_t *) node);

    return NJS_OK;
}


njs_bool_t
njs_variable_closure_test(njs_parser_scope_t *root, njs_parser_scope_t *scope)
{
    if (root == scope) {
        return 0;
    }

    do {
        if (root->type == NJS_SCOPE_FUNCTION) {
            return 1;
        }

        root = root->parent;

    } while (root != scope);

    return 0;
}


njs_variable_t *
njs_variable_resolve(njs_vm_t *vm, njs_parser_node_t *node)
{
    njs_rbtree_node_t         *rb_node;
    njs_parser_scope_t        *scope;
    njs_variable_node_t       var_node;
    njs_variable_reference_t  *ref;

    ref = &node->u.reference;
    scope = node->scope;

    var_node.key = ref->atom_id;

    do {
        rb_node = njs_rbtree_find(&scope->variables, &var_node.node);

        if (rb_node != NULL) {
            return ((njs_variable_node_t *) rb_node)->variable;
        }

        scope = scope->parent;

    } while (scope != NULL);

    return NULL;
}


static njs_index_t
njs_variable_closure(njs_vm_t *vm, njs_variable_t *var,
    njs_parser_scope_t *scope)
{
    njs_index_t               index, prev_index, *idx;
    njs_level_type_t          type;
    njs_rbtree_node_t         *rb_node;
    njs_parser_scope_t        **p;
    njs_parser_rbtree_node_t  *parse_node, ref_node;
#define NJS_VAR_MAX_DEPTH     32
    njs_parser_scope_t        *list[NJS_VAR_MAX_DEPTH];

    ref_node.key = var->atom_id;

    p = list;

    do {
        if (njs_slow_path(p == &list[NJS_VAR_MAX_DEPTH - 1])) {
            njs_error(vm, "maximum depth of nested functions is reached");
            return NJS_INDEX_ERROR;
        }

        if (scope->type == NJS_SCOPE_FUNCTION) {
            *p++ = scope;
        }

        scope = scope->parent;

    } while (scope != var->scope && scope->type != NJS_SCOPE_GLOBAL);

    prev_index = var->index;

    while (p != list) {
        p--;

        scope = *p;

        rb_node = njs_rbtree_find(&scope->references, &ref_node.node);

        parse_node = ((njs_parser_rbtree_node_t *) rb_node);

        type = NJS_LEVEL_LOCAL;

        if (parse_node != NULL) {
            type = njs_scope_index_type(parse_node->index);

            if (p != list && parse_node->index != 0) {
                prev_index = parse_node->index;
                continue;
            }
        }

        if (type != NJS_LEVEL_CLOSURE) {
            /* Create new closure for scope. */

            index = njs_scope_index(scope->type, scope->closures->items,
                                    NJS_LEVEL_CLOSURE, var->type);
            if (njs_slow_path(index == NJS_INDEX_ERROR)) {
                return NJS_INDEX_ERROR;
            }

            idx = njs_arr_add(scope->closures);
            if (njs_slow_path(idx == NULL)) {
                return NJS_INDEX_ERROR;
            }

            *idx = prev_index;

            if (parse_node == NULL) {
                /* Create new reference for closure. */

                parse_node = njs_mp_alloc(vm->mem_pool,
                                          sizeof(njs_parser_rbtree_node_t));
                if (njs_slow_path(parse_node == NULL)) {
                    return NJS_INDEX_ERROR;
                }

                parse_node->key = var->atom_id;

                njs_rbtree_insert(&scope->references, &parse_node->node);
            }

            parse_node->index = index;
        }

        prev_index = parse_node->index;
    }

    return prev_index;
}


njs_variable_t *
njs_variable_reference(njs_vm_t *vm, njs_parser_node_t *node)
{
    njs_bool_t                closure;
    njs_rbtree_node_t         *rb_node;
    njs_parser_scope_t        *scope;
    njs_parser_rbtree_node_t  *parse_node, ref_node;
    njs_variable_reference_t  *ref;

    ref = &node->u.reference;
    scope = node->scope;

    if (ref->variable == NULL) {
        ref->variable = njs_variable_resolve(vm, node);
        if (njs_slow_path(ref->variable == NULL)) {
            ref->not_defined = 1;

            return NULL;
        }
    }

    closure = njs_variable_closure_test(node->scope, ref->variable->scope);
    ref->scope = node->scope;

    ref_node.key = ref->atom_id;

    rb_node = njs_rbtree_find(&scope->references, &ref_node.node);
    if (njs_slow_path(rb_node == NULL)) {
        return NULL;
    }

    parse_node = ((njs_parser_rbtree_node_t *) rb_node);

    if (parse_node->index != NJS_INDEX_NONE) {
        node->index = parse_node->index;

        return ref->variable;
    }

    if (!closure) {
        node->index = ref->variable->index;

        return ref->variable;
    }

    ref->variable->closure = closure;

    node->index = njs_variable_closure(vm, ref->variable, scope);
    if (njs_slow_path(node->index == NJS_INDEX_ERROR)) {
        return NULL;
    }

    return ref->variable;
}


njs_variable_t *
njs_label_find(njs_vm_t *vm, njs_parser_scope_t *scope, uintptr_t atom_id)
{
    njs_rbtree_node_t    *node;
    njs_variable_node_t  var_node;

    var_node.key = atom_id;

    do {
        node = njs_rbtree_find(&scope->labels, &var_node.node);

        if (node != NULL) {
            return ((njs_variable_node_t *) node)->variable;
        }

        scope = scope->parent;

    } while (scope != NULL);

    return NULL;
}


static njs_variable_t *
njs_variable_alloc(njs_vm_t *vm, uintptr_t atom_id, njs_variable_type_t type)
{
    njs_variable_t  *var;

    var = njs_mp_zalloc(vm->mem_pool, sizeof(njs_variable_t));
    if (njs_slow_path(var == NULL)) {
        njs_memory_error(vm);
        return NULL;
    }

    var->atom_id = atom_id;
    var->type = type;

    return var;
}


njs_int_t
njs_name_copy(njs_vm_t *vm, njs_str_t *dst, const njs_str_t *src)
{
    dst->length = src->length;

    dst->start = njs_mp_alloc(vm->mem_pool, src->length);

    if (njs_fast_path(dst->start != NULL)) {
        (void) memcpy(dst->start, src->start, src->length);

        return NJS_OK;
    }

    njs_memory_error(vm);

    return NJS_ERROR;
}
