
/*
 * Copyright (C) Artem S. Povalyukhin
 * Copyright (C) NGINX, Inc.
 */


#include <njs_main.h>


struct njs_value_iterator_s {
    njs_value_t        target;
    int64_t            next;
    njs_object_enum_t  kind;
};


typedef struct njs_value_iterator_s  njs_array_iterator_t;


static const njs_value_t  string_done = njs_string("done");
static const njs_value_t  string_value = njs_string("value");


static njs_int_t njs_iterator_object_handler(njs_vm_t *vm,
    njs_iterator_handler_t handler, njs_iterator_args_t *args,
    njs_value_t *key, int64_t i);

static njs_int_t njs_iterator_to_array_handler(njs_vm_t *vm,
    njs_iterator_args_t *args, njs_value_t *value, int64_t index);


njs_int_t
njs_array_iterator_create(njs_vm_t *vm, const njs_value_t *target,
    njs_value_t *retval, njs_object_enum_t kind)
{
    njs_object_value_t    *ov;
    njs_array_iterator_t  *it;

    ov = njs_mp_alloc(vm->mem_pool, sizeof(njs_object_value_t));
    if (njs_slow_path(ov == NULL)) {
        njs_memory_error(vm);
        return NJS_ERROR;
    }

    njs_lvlhsh_init(&ov->object.hash);
    njs_lvlhsh_init(&ov->object.shared_hash);
    ov->object.type = NJS_OBJECT_VALUE;
    ov->object.shared = 0;
    ov->object.extensible = 1;
    ov->object.error_data = 0;
    ov->object.fast_array = 0;

    ov->object.__proto__ =
        &vm->prototypes[NJS_OBJ_TYPE_ARRAY_ITERATOR].object;
    ov->object.slots = NULL;

    it = njs_mp_alloc(vm->mem_pool, sizeof(njs_array_iterator_t));
    if (njs_slow_path(it == NULL)) {
        njs_memory_error(vm);
        return NJS_ERROR;
    }

    /* GC retain it->target */
    it->target = *target;
    it->next = 0;
    it->kind = kind;

    njs_set_data(&ov->value, it, NJS_DATA_TAG_ARRAY_ITERATOR);
    njs_set_object_value(retval, ov);

    return NJS_OK;
}


njs_int_t
njs_array_iterator_next(njs_vm_t *vm, njs_value_t *iterator,
    njs_value_t *retval)
{
    int64_t               length;
    njs_int_t             ret;
    njs_array_t           *array, *entry;
    njs_typed_array_t     *tarray;
    const njs_value_t     *value;
    njs_array_iterator_t  *it;

    if (njs_slow_path(!njs_is_valid(njs_object_value(iterator)))) {
        return NJS_DECLINED;
    }

    it = njs_object_data(iterator);
    value = &njs_value_undefined;

    if (njs_is_fast_array(&it->target)) {
        array = njs_array(&it->target);
        length = array->length;

        if (it->next >= length) {
            goto release;
        }

        if (it->kind > NJS_ENUM_KEYS && njs_is_valid(&array->start[it->next])) {
            value = &array->start[it->next];
        }

    } else if (njs_is_typed_array(&it->target)) {
        tarray = njs_typed_array(&it->target);

        if (njs_slow_path(njs_is_detached_buffer(tarray->buffer))) {
            njs_type_error(vm, "detached buffer");
            return NJS_ERROR;
        }

        length = njs_typed_array_length(tarray);

        if (it->next >= length) {
            goto release;
        }

        if (it->kind > NJS_ENUM_KEYS) {
            njs_set_number(retval, njs_typed_array_prop(tarray, it->next));
            value = retval;
        }

    } else {
        ret = njs_object_length(vm, &it->target, &length);
        if (njs_slow_path(ret == NJS_ERROR)) {
            return ret;
        }

        if (it->next >= length) {
            goto release;
        }

        if (it->kind > NJS_ENUM_KEYS) {
            ret = njs_value_property_i64(vm, &it->target, it->next, retval);
            if (njs_slow_path(ret == NJS_ERROR)) {
                return ret;
            }

            value = njs_is_valid(retval) ? retval
                                         : &njs_value_undefined;
        }
    }

    switch (it->kind) {
    case NJS_ENUM_KEYS:
        njs_set_number(retval, it->next++);
        break;

    case NJS_ENUM_VALUES:
        it->next++;
        *retval = *value;
        break;

    case NJS_ENUM_BOTH:
        entry = njs_array_alloc(vm, 0, 2, 0);
        if (njs_slow_path(entry == NULL)) {
            return NJS_ERROR;
        }

        njs_set_number(&entry->start[0], it->next++);
        entry->start[1] = *value;

        njs_set_array(retval, entry);
        break;

    default:
        njs_internal_error(vm, "invalid enum kind");
        return NJS_ERROR;
    }

    return NJS_OK;

release:

    /* GC release it->target */
    njs_mp_free(vm->mem_pool, it);
    njs_set_invalid(njs_object_value(iterator));

    return NJS_DECLINED;
}


static njs_int_t
njs_iterator_prototype_get_this(njs_vm_t *vm, njs_value_t *args,
    njs_uint_t nargs, njs_index_t unused)
{
    vm->retval = args[0];

    return NJS_OK;
}


static const njs_object_prop_t  njs_iterator_prototype_properties[] =
{
    {
        .type = NJS_PROPERTY,
        .name = njs_wellknown_symbol(NJS_SYMBOL_ITERATOR),
        .value = njs_native_function(njs_iterator_prototype_get_this, 0),
        .configurable = 1,
        .writable = 1,
    },
};


static const njs_object_init_t  njs_iterator_prototype_init = {
    njs_iterator_prototype_properties,
    njs_nitems(njs_iterator_prototype_properties),
};


const njs_object_type_init_t  njs_iterator_type_init = {
    .prototype_props = &njs_iterator_prototype_init,
    .prototype_value = { .object = { .type = NJS_OBJECT } },
};


static njs_int_t
njs_array_iterator_prototype_next(njs_vm_t *vm, njs_value_t *args,
    njs_uint_t nargs, njs_index_t tag)
{
    njs_int_t          ret;
    njs_bool_t         check;
    njs_value_t        *this;
    njs_object_t       *object;
    njs_object_prop_t  *prop_value, *prop_done;

    this = njs_argument(args, 0);

    check = njs_is_object_value(this)
            && (njs_is_object_data(this, NJS_DATA_TAG_ARRAY_ITERATOR)
                || !njs_is_valid(njs_object_value(this)));

    if (njs_slow_path(!check)) {
        njs_type_error(vm, "Method [Array Iterator].prototype.next"
                           " called on incompatible receiver");
        return NJS_ERROR;
    }

    object = njs_object_alloc(vm);
    if (njs_slow_path(object == NULL)) {
        return NJS_ERROR;
    }

    njs_set_object(&vm->retval, object);

    prop_value = njs_object_property_add(vm, &vm->retval,
                                         njs_value_arg(&string_value), 0);
    if (njs_slow_path(prop_value == NULL)) {
        return NJS_ERROR;
    }

    prop_done = njs_object_property_add(vm, &vm->retval,
                                        njs_value_arg(&string_done), 0);
    if (njs_slow_path(prop_done == NULL)) {
        return NJS_ERROR;
    }

    ret = njs_array_iterator_next(vm, this, &prop_value->value);
    if (njs_slow_path(ret == NJS_ERROR)) {
        return ret;
    }

    if (njs_slow_path(ret == NJS_DECLINED)) {
        njs_set_undefined(&prop_value->value);
        njs_set_boolean(&prop_done->value, 1);

        return NJS_OK;
    }

    njs_set_boolean(&prop_done->value, 0);

    return NJS_OK;
}


static const njs_object_prop_t  njs_array_iterator_prototype_properties[] =
{
    {
        .type = NJS_PROPERTY,
        .name = njs_string("next"),
        .value = njs_native_function2(njs_array_iterator_prototype_next, 0,
                                      NJS_DATA_TAG_ARRAY_ITERATOR),
        .configurable = 1,
        .writable = 1,
    },

    {
        .type = NJS_PROPERTY,
        .name = njs_wellknown_symbol(NJS_SYMBOL_TO_STRING_TAG),
        .value = njs_string("Array Iterator"),
        .configurable = 1,
    },
};


static const njs_object_init_t  njs_array_iterator_prototype_init = {
    njs_array_iterator_prototype_properties,
    njs_nitems(njs_array_iterator_prototype_properties),
};


const njs_object_type_init_t  njs_array_iterator_type_init = {
    .prototype_props = &njs_array_iterator_prototype_init,
    .prototype_value = { .object = { .type = NJS_OBJECT } },
};


njs_int_t
njs_object_iterate(njs_vm_t *vm, njs_iterator_args_t *args,
    njs_iterator_handler_t handler)
{
    double             idx;
    int64_t            length, i, from, to;
    njs_int_t          ret;
    njs_array_t        *array, *keys;
    njs_value_t        *value, *entry, prop, character, string_obj;
    njs_object_t       *object;
    const u_char       *p, *end, *pos;
    njs_string_prop_t  string_prop;

    value = args->value;
    from = args->from;
    to = args->to;

    if (njs_is_array(value)) {
        array = njs_array(value);

        for (; from < to; from++) {
            if (njs_slow_path(!array->object.fast_array)) {
                goto process_object;
            }

            if (njs_fast_path(from < array->length
                              && njs_is_valid(&array->start[from])))
            {
                ret = handler(vm, args, &array->start[from], from);

            } else {
                entry = njs_value_arg(&njs_value_invalid);
                ret = njs_value_property_i64(vm, value, from, &prop);
                if (njs_slow_path(ret != NJS_DECLINED)) {
                    if (ret == NJS_ERROR) {
                        return NJS_ERROR;
                    }

                    entry = &prop;
                }

                ret = handler(vm, args, entry, from);
            }

            if (njs_slow_path(ret != NJS_OK)) {
                if (ret == NJS_DONE) {
                    return NJS_DONE;
                }

                return NJS_ERROR;
            }
        }

        return NJS_OK;
    }

    if (njs_is_string(value) || njs_is_object_string(value)) {

        if (njs_is_string(value)) {
            object = njs_object_value_alloc(vm, value, NJS_STRING);
            if (njs_slow_path(object == NULL)) {
                return NJS_ERROR;
            }

            njs_set_type_object(&string_obj, object, NJS_OBJECT_STRING);

            args->value = &string_obj;
        }
        else {
            value = njs_object_value(value);
        }

        length = njs_string_prop(&string_prop, value);

        p = string_prop.start;
        end = p + string_prop.size;

        if ((size_t) length == string_prop.size) {
            /* Byte or ASCII string. */

            for (i = from; i < to; i++) {
                /* This cannot fail. */
                (void) njs_string_new(vm, &character, p + i, 1, 1);

                ret = handler(vm, args, &character, i);
                if (njs_slow_path(ret != NJS_OK)) {
                    if (ret == NJS_DONE) {
                        return NJS_DONE;
                    }

                    return NJS_ERROR;
                }
            }

        } else {
            /* UTF-8 string. */

            for (i = from; i < to; i++) {
                pos = njs_utf8_next(p, end);

                /* This cannot fail. */
                (void) njs_string_new(vm, &character, p, pos - p, 1);

                ret = handler(vm, args, &character, i);
                if (njs_slow_path(ret != NJS_OK)) {
                    if (ret == NJS_DONE) {
                        return NJS_DONE;
                    }

                    return NJS_ERROR;
                }

                p = pos;
            }
        }

        return NJS_OK;
    }

    if (!njs_is_object(value)) {
        return NJS_OK;
    }

process_object:

    if (!njs_fast_object(to - from)) {
        keys = njs_array_indices(vm, value);
        if (njs_slow_path(keys == NULL)) {
            return NJS_ERROR;
        }

        for (i = 0; i < keys->length; i++) {
            idx = njs_string_to_index(&keys->start[i]);

            if (idx < from || idx >= to) {
                continue;
            }

            ret = njs_iterator_object_handler(vm, handler, args, &keys->start[i],
                                           idx);
            if (njs_slow_path(ret != NJS_OK)) {
                njs_array_destroy(vm, keys);
                return ret;
            }
        }

        njs_array_destroy(vm, keys);

        return NJS_OK;
    }

    for (i = from; i < to; i++) {
        ret = njs_iterator_object_handler(vm, handler, args, NULL, i);
        if (njs_slow_path(ret != NJS_OK)) {
            return ret;
        }
    }

    return NJS_OK;
}


njs_int_t
njs_object_iterate_reverse(njs_vm_t *vm, njs_iterator_args_t *args,
    njs_iterator_handler_t handler)
{
    double             idx;
    int64_t            i, from, to, length;
    njs_int_t          ret;
    njs_array_t        *array, *keys;
    njs_value_t        *entry, *value, prop, character, string_obj;
    njs_object_t       *object;
    const u_char       *p, *end, *pos;
    njs_string_prop_t  string_prop;

    value = args->value;
    from = args->from;
    to = args->to;

    if (njs_is_array(value)) {
        array = njs_array(value);

        from += 1;

        while (from-- > to) {
            if (njs_slow_path(!array->object.fast_array)) {
                goto process_object;
            }

            if (njs_fast_path(from < array->length
                              && njs_is_valid(&array->start[from])))
            {
                ret = handler(vm, args, &array->start[from], from);

            } else {
                entry = njs_value_arg(&njs_value_invalid);
                ret = njs_value_property_i64(vm, value, from, &prop);
                if (njs_slow_path(ret != NJS_DECLINED)) {
                    if (ret == NJS_ERROR) {
                        return NJS_ERROR;
                    }

                    entry = &prop;
                }

                ret = handler(vm, args, entry, from);
            }

            if (njs_slow_path(ret != NJS_OK)) {
                if (ret == NJS_DONE) {
                    return NJS_DONE;
                }

                return NJS_ERROR;
            }
        }

        return NJS_OK;
    }

    if (njs_is_string(value) || njs_is_object_string(value)) {

        if (njs_is_string(value)) {
            object = njs_object_value_alloc(vm, value, NJS_STRING);
            if (njs_slow_path(object == NULL)) {
                return NJS_ERROR;
            }

            njs_set_type_object(&string_obj, object, NJS_OBJECT_STRING);

            args->value = &string_obj;
        }
        else {
            value = njs_object_value(value);
        }

        length = njs_string_prop(&string_prop, value);
        end = string_prop.start + string_prop.size;

        if ((size_t) length == string_prop.size) {
            /* Byte or ASCII string. */

            p = string_prop.start + from;

            i = from + 1;

            while (i-- > to) {
                /* This cannot fail. */
                (void) njs_string_new(vm, &character, p, 1, 1);

                ret = handler(vm, args, &character, i);
                if (njs_slow_path(ret != NJS_OK)) {
                    if (ret == NJS_DONE) {
                        return NJS_DONE;
                    }

                    return NJS_ERROR;
                }

                p--;
            }

        } else {
            /* UTF-8 string. */

            p = njs_string_offset(string_prop.start, end, from);
            p = njs_utf8_next(p, end);

            i = from + 1;

            while (i-- > to) {
                pos = njs_utf8_prev(p);

                /* This cannot fail. */
                (void) njs_string_new(vm, &character, pos, p - pos , 1);

                ret = handler(vm, args, &character, i);
                if (njs_slow_path(ret != NJS_OK)) {
                    if (ret == NJS_DONE) {
                        return NJS_DONE;
                    }

                    return NJS_ERROR;
                }

                p = pos;
            }
        }

        return NJS_OK;
    }

    if (!njs_is_object(value)) {
        return NJS_OK;
    }

process_object:

    if (!njs_fast_object(from - to)) {
        keys = njs_array_indices(vm, value);
        if (njs_slow_path(keys == NULL)) {
            return NJS_ERROR;
        }

        i = keys->length;

        while (i > 0) {
            idx = njs_string_to_index(&keys->start[--i]);

            if (idx < to || idx > from) {
                continue;
            }

            ret = njs_iterator_object_handler(vm, handler, args,
                                              &keys->start[i], idx);
            if (njs_slow_path(ret != NJS_OK)) {
                njs_array_destroy(vm, keys);
                return ret;
            }
        }

        njs_array_destroy(vm, keys);

        return NJS_OK;
    }

    i = from + 1;

    while (i-- > to) {
        ret = njs_iterator_object_handler(vm, handler, args, NULL, i);
        if (njs_slow_path(ret != NJS_OK)) {
            return ret;
        }
    }

    return NJS_OK;
}


static njs_int_t
njs_iterator_object_handler(njs_vm_t *vm, njs_iterator_handler_t handler,
    njs_iterator_args_t *args, njs_value_t *key, int64_t i)
{
    njs_int_t    ret;
    njs_value_t  prop, *entry;

    if (key != NULL) {
        ret = njs_value_property(vm, args->value, key, &prop);
        if (njs_slow_path(ret == NJS_ERROR)) {
            return ret;
        }

    } else {
        ret = njs_value_property_i64(vm, args->value, i, &prop);
        if (njs_slow_path(ret == NJS_ERROR)) {
            return ret;
        }
    }

    entry = (ret == NJS_OK) ? &prop : njs_value_arg(&njs_value_invalid);

    ret = handler(vm, args, entry, i);
    if (njs_slow_path(ret != NJS_OK)) {
        if (ret == NJS_DONE) {
            return NJS_DONE;
        }

        return NJS_ERROR;
    }

    return ret;
}


njs_array_t *
njs_iterator_to_array(njs_vm_t *vm, njs_value_t *iterator)
{
    int64_t              length;
    njs_int_t            ret;
    njs_iterator_args_t  args;

    njs_memzero(&args, sizeof(njs_iterator_args_t));

    ret = njs_object_length(vm, iterator, &length);
    if (njs_slow_path(ret != NJS_OK)) {
        return NULL;
    }

    args.data = njs_array_alloc(vm, 1, length, 0);
    if (njs_slow_path(args.data == NULL)) {
        return NULL;
    }

    args.value = iterator;
    args.to = length;

    ret = njs_object_iterate(vm, &args, njs_iterator_to_array_handler);
    if (njs_slow_path(ret == NJS_ERROR)) {
        njs_mp_free(vm->mem_pool, args.data);
        return NULL;
    }

    return args.data;
}


static njs_int_t
njs_iterator_to_array_handler(njs_vm_t *vm, njs_iterator_args_t *args,
    njs_value_t *value, int64_t index)
{
    njs_array_t  *array;

    array = args->data;
    array->start[index] = *value;

    return NJS_OK;
}
