// Copyright (C) 2018 Crimson AS <info@crimson.no>
// Copyright (C) 2016 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only

#include "qv4arrayobject_p.h"
#include "qv4arrayiterator_p.h"
#include "qv4objectproto_p.h"
#include "qv4argumentsobject_p.h"
#include "qv4runtime_p.h"
#include "qv4symbol_p.h"
#include <QtCore/qscopedvaluerollback.h>

using namespace QV4;

DEFINE_OBJECT_VTABLE(ArrayCtor);

void Heap::ArrayCtor::init(QV4::ExecutionEngine *engine)
{
    Heap::FunctionObject::init(engine, QStringLiteral("Array"));
}

ReturnedValue ArrayCtor::virtualCallAsConstructor(const FunctionObject *f, const Value *argv, int argc, const Value *newTarget)
{
    ExecutionEngine *v4 = static_cast<const ArrayCtor *>(f)->engine();
    Scope scope(v4);
    ScopedArrayObject a(scope, v4->newArrayObject());
    if (newTarget)
        a->setProtoFromNewTarget(newTarget);
    uint len;
    if (argc == 1 && argv[0].isNumber()) {
        bool ok;
        len = argv[0].asArrayLength(&ok);

        if (!ok)
            return v4->throwRangeError(argv[0]);

        if (len < 0x1000)
            a->arrayReserve(len);
    } else {
        len = argc;
        a->arrayReserve(len);
        a->arrayPut(0, argv, len);
    }
    a->setArrayLengthUnchecked(len);

    return a.asReturnedValue();
}

ReturnedValue ArrayCtor::virtualCall(const FunctionObject *f, const Value *, const Value *argv, int argc)
{
    return virtualCallAsConstructor(f, argv, argc, f);
}

void ArrayPrototype::init(ExecutionEngine *engine, Object *ctor)
{
    Scope scope(engine);
    ScopedObject o(scope);
    ctor->defineReadonlyConfigurableProperty(engine->id_length(), Value::fromInt32(1));
    ctor->defineReadonlyProperty(engine->id_prototype(), (o = this));
    ctor->defineDefaultProperty(QStringLiteral("isArray"), method_isArray, 1);
    ctor->defineDefaultProperty(QStringLiteral("of"), method_of, 0);
    ctor->defineDefaultProperty(QStringLiteral("from"), method_from, 1);
    ctor->addSymbolSpecies();

    Scoped<InternalClass> ic(scope, engine->classes[EngineBase::Class_Empty]
                                            ->changeVTable(QV4::Object::staticVTable()));
    ScopedObject unscopables(scope, engine->newObject(ic->d()));
    ScopedString name(scope);
    defineDefaultProperty(QStringLiteral("constructor"), (o = ctor));
    defineDefaultProperty(engine->id_toString(), method_toString, 0);
    defineDefaultProperty(engine->id_toLocaleString(), method_toLocaleString, 0);
    defineDefaultProperty(QStringLiteral("concat"), method_concat, 1);
    name = engine->newIdentifier(QStringLiteral("copyWithin"));
    unscopables->put(name, Value::fromBoolean(true));
    defineDefaultProperty(name, method_copyWithin, 2);
    name = engine->newIdentifier(QStringLiteral("entries"));
    unscopables->put(name, Value::fromBoolean(true));
    defineDefaultProperty(name, method_entries, 0);
    name = engine->newIdentifier(QStringLiteral("fill"));
    unscopables->put(name, Value::fromBoolean(true));
    defineDefaultProperty(name, method_fill, 1);
    name = engine->newIdentifier(QStringLiteral("find"));
    unscopables->put(name, Value::fromBoolean(true));
    defineDefaultProperty(name, method_find, 1);
    name = engine->newIdentifier(QStringLiteral("findIndex"));
    unscopables->put(name, Value::fromBoolean(true));
    defineDefaultProperty(name, method_findIndex, 1);
    name = engine->newIdentifier(QStringLiteral("includes"));
    unscopables->put(name, Value::fromBoolean(true));
    defineDefaultProperty(name, method_includes, 1);
    defineDefaultProperty(QStringLiteral("join"), method_join, 1);
    name = engine->newIdentifier(QStringLiteral("keys"));
    unscopables->put(name, Value::fromBoolean(true));
    defineDefaultProperty(name, method_keys, 0);
    defineDefaultProperty(QStringLiteral("pop"), method_pop, 0);
    defineDefaultProperty(QStringLiteral("push"), method_push, 1);
    defineDefaultProperty(QStringLiteral("reverse"), method_reverse, 0);
    defineDefaultProperty(QStringLiteral("shift"), method_shift, 0);
    defineDefaultProperty(QStringLiteral("slice"), method_slice, 2);
    defineDefaultProperty(QStringLiteral("sort"), method_sort, 1);
    defineDefaultProperty(QStringLiteral("splice"), method_splice, 2);
    defineDefaultProperty(QStringLiteral("unshift"), method_unshift, 1);
    defineDefaultProperty(QStringLiteral("indexOf"), method_indexOf, 1);
    defineDefaultProperty(QStringLiteral("lastIndexOf"), method_lastIndexOf, 1);
    defineDefaultProperty(QStringLiteral("every"), method_every, 1);
    defineDefaultProperty(QStringLiteral("some"), method_some, 1);
    defineDefaultProperty(QStringLiteral("forEach"), method_forEach, 1);
    defineDefaultProperty(QStringLiteral("map"), method_map, 1);
    defineDefaultProperty(QStringLiteral("filter"), method_filter, 1);
    defineDefaultProperty(QStringLiteral("reduce"), method_reduce, 1);
    defineDefaultProperty(QStringLiteral("reduceRight"), method_reduceRight, 1);
    ScopedString valuesString(scope, engine->newIdentifier(QStringLiteral("values")));
    ScopedObject values(scope, FunctionObject::createBuiltinFunction(engine, valuesString, method_values, 0));
    engine->jsObjects[ExecutionEngine::ArrayProtoValues] = values;
    unscopables->put(valuesString, Value::fromBoolean(true));
    defineDefaultProperty(valuesString, values);
    defineDefaultProperty(engine->symbol_iterator(), values);

    defineReadonlyConfigurableProperty(engine->symbol_unscopables(), unscopables);
}

ReturnedValue ArrayPrototype::method_isArray(const FunctionObject *, const Value *, const Value *argv, int argc)
{
    if (!argc || !argv->objectValue())
        return Encode(false);
    return Encode(argv->objectValue()->isArray());
}

static ScopedObject createObjectFromCtorOrArray(Scope &scope, ScopedFunctionObject ctor, bool useLen, int len)
{
    ScopedObject a(scope, Value::undefinedValue());

    if (ctor && ctor->isConstructor()) {
        // this isn't completely kosher. for instance:
        // Array.from.call(Object, []).constructor == Object
        // is expected by the tests, but naturally, we get Number.
        ScopedValue argument(scope, useLen ? Value::fromReturnedValue(QV4::Encode(len))
                                           : Value::undefinedValue());
        a = ctor->callAsConstructor(argument, useLen ? 1 : 0);
    } else {
        a = scope.engine->newArrayObject(len);
    }

    return a;
}

ReturnedValue ArrayPrototype::method_from(const FunctionObject *builtin, const Value *thisObject, const Value *argv, int argc)
{
    Scope scope(builtin);
    ScopedFunctionObject thatCtor(scope, thisObject);
    ScopedObject itemsObject(scope, argv[0]);
    bool usingIterator = false;

    if (itemsObject) {
        // If the object claims to support iterators, then let's try use them.
        ScopedValue it(scope, itemsObject->get(scope.engine->symbol_iterator()));
        if (!it->isNullOrUndefined()) {
            ScopedFunctionObject itfunc(scope, it);
            if (!itfunc)
                return scope.engine->throwTypeError();
            usingIterator = true;
        }
    }

    ScopedFunctionObject mapfn(scope, Value::undefinedValue());
    Value *mapArguments = nullptr;
    if (argc > 1) {
        mapfn = ScopedFunctionObject(scope, argv[1]);
        if (!mapfn)
            return scope.engine->throwTypeError(QString::fromLatin1("%1 is not a function").arg(argv[1].toQStringNoThrow()));
        mapArguments = scope.alloc(2);
    }

    ScopedValue thisArg(scope);
    if (argc > 2)
        thisArg = argv[2];

    if (usingIterator) {
        // Item iteration supported, so let's go ahead and try use that.
        ScopedObject a(createObjectFromCtorOrArray(scope, thatCtor, false, 0));
        CHECK_EXCEPTION();
        ScopedObject iterator(scope, Runtime::GetIterator::call(scope.engine, itemsObject, true));
        CHECK_EXCEPTION(); // symbol_iterator threw; whoops.
        if (!iterator) {
            return scope.engine->throwTypeError(); // symbol_iterator wasn't an object.
        }

        qint64 k = 0;
        ScopedValue mappedValue(scope);
        Value *nextValue = scope.alloc(1);
        ScopedValue done(scope);

        // The loop below pulls out all the properties using the iterator, and
        // sets them into the created array.
        forever {
            if (k > (static_cast<qint64>(1) << 53) - 1) {
                ScopedValue error(scope, scope.engine->throwTypeError());
                return Runtime::IteratorClose::call(scope.engine, iterator);
            }

            // Retrieve the next value. If the iteration ends, we're done here.
            done = Value::fromReturnedValue(Runtime::IteratorNext::call(scope.engine, iterator, nextValue));
            CHECK_EXCEPTION();
            if (done->toBoolean()) {
                if (ArrayObject *ao = a->as<ArrayObject>()) {
                    ao->setArrayLengthUnchecked(k);
                } else {
                    a->set(scope.engine->id_length(), Value::fromDouble(k), QV4::Object::DoThrowOnRejection);
                    CHECK_EXCEPTION();
                }
                return a.asReturnedValue();
            }

            if (mapfn) {
                Q_ASSERT(mapArguments); // if mapfn is set, we always setup mapArguments with scope.alloc
                mapArguments[0] = *nextValue;
                mapArguments[1] = Value::fromDouble(k);
                mappedValue = mapfn->call(thisArg, mapArguments, 2);
                if (scope.hasException())
                    return Runtime::IteratorClose::call(scope.engine, iterator);
            } else {
                mappedValue = *nextValue;
            }

            if (a->getOwnProperty(PropertyKey::fromArrayIndex(k)) == Attr_Invalid) {
                a->arraySet(k, mappedValue);
            } else {
                // Don't return: we need to close the iterator.
                scope.engine->throwTypeError(QString::fromLatin1("Cannot redefine property: %1").arg(k));
            }

            if (scope.hasException())
                return Runtime::IteratorClose::call(scope.engine, iterator);

            k++;
        }

        // the return is hidden up in the loop above, when iteration finishes.
    } else {
        // Array-like fallback. We request properties by index, and set them on
        // the return object.
        ScopedObject arrayLike(scope, argv[0].toObject(scope.engine));
        if (!arrayLike)
            return scope.engine->throwTypeError(QString::fromLatin1("Cannot convert %1 to object").arg(argv[0].toQStringNoThrow()));
        qint64 len = arrayLike->getLength();
        ScopedObject a(createObjectFromCtorOrArray(scope, thatCtor, true, len));
        CHECK_EXCEPTION();

        qint64 k = 0;
        ScopedValue mappedValue(scope, Value::undefinedValue());
        ScopedValue kValue(scope);
        while (k < len) {
            kValue = arrayLike->get(k);
            CHECK_EXCEPTION();

            if (mapfn) {
                Q_ASSERT(mapArguments); // if mapfn is set, we always setup mapArguments with scope.alloc
                mapArguments[0] = kValue;
                mapArguments[1] = Value::fromDouble(k);
                mappedValue = mapfn->call(thisArg, mapArguments, 2);
                CHECK_EXCEPTION();
            } else {
                mappedValue = kValue;
            }

            if (a->getOwnProperty(PropertyKey::fromArrayIndex(k)) != Attr_Invalid)
                return scope.engine->throwTypeError(QString::fromLatin1("Cannot redefine property: %1").arg(k));

            a->arraySet(k, mappedValue);
            CHECK_EXCEPTION();

            k++;
        }

        if (ArrayObject *ao = a->as<ArrayObject>()) {
            ao->setArrayLengthUnchecked(k);
        } else {
            a->set(scope.engine->id_length(), Value::fromDouble(k), QV4::Object::DoThrowOnRejection);
            CHECK_EXCEPTION();
        }
        return a.asReturnedValue();
    }

}

ReturnedValue ArrayPrototype::method_of(const FunctionObject *builtin, const Value *thisObject, const Value *argv, int argc)
{
    Scope scope(builtin);
    ScopedFunctionObject that(scope, thisObject);
    ScopedObject a(createObjectFromCtorOrArray(scope, that, true, argc));
    CHECK_EXCEPTION();

    int k = 0;
    while (k < argc) {
        if (a->getOwnProperty(PropertyKey::fromArrayIndex(k)) != Attr_Invalid) {
            return scope.engine->throwTypeError(QString::fromLatin1("Cannot redefine property: %1").arg(k));
        }
        a->arraySet(k, argv[k]);
        CHECK_EXCEPTION();

        k++;
    }

    // ArrayObject updates its own length, and will throw if we try touch it.
    if (!a->as<ArrayObject>()) {
        a->set(scope.engine->id_length(), Value::fromDouble(argc), QV4::Object::DoThrowOnRejection);
        CHECK_EXCEPTION();
    }

    return a.asReturnedValue();
}

ReturnedValue ArrayPrototype::method_toString(const FunctionObject *builtin, const Value *thisObject, const Value *argv, int argc)
{
    Scope scope(builtin);
    ScopedObject that(scope, thisObject->toObject(scope.engine));
    if (scope.hasException())
        return QV4::Encode::undefined();

    ScopedString string(scope, scope.engine->newString(QStringLiteral("join")));
    ScopedFunctionObject f(scope, that->get(string));
    if (f)
        return checkedResult(scope.engine, f->call(that, argv, argc));
    return ObjectPrototype::method_toString(builtin, that, argv, argc);
}

ReturnedValue ArrayPrototype::method_toLocaleString(const FunctionObject *b, const Value *thisObject, const Value *, int)
{
    Scope scope(b);
    ScopedObject instance(scope, thisObject);
    if (!instance)
        return scope.engine->throwTypeError();

    uint len = instance->getLength();
    const QString separator = QStringLiteral(",");

    QString R;

    ScopedValue v(scope);
    ScopedString s(scope);

    ScopedPropertyKey tolocaleString(scope, scope.engine->id_toLocaleString()->toPropertyKey());
    Q_ASSERT(!scope.engine->hasException);

    for (uint k = 0; k < len; ++k) {
        if (k)
            R += separator;

        v = instance->get(k);
        if (v->isNullOrUndefined())
            continue;

        ScopedObject valueAsObject(scope, v->toObject(scope.engine));
        Q_ASSERT(valueAsObject); // null and undefined handled above

        ScopedFunctionObject function(scope, valueAsObject->get(tolocaleString));
        if (!function)
            return scope.engine->throwTypeError();

        v = function->call(valueAsObject, nullptr, 0);
        if (scope.hasException())
            return Encode::undefined();

        s = v->toString(scope.engine);
        if (scope.hasException())
            return Encode::undefined();

        R += s->toQString();
    }
    return scope.engine->newString(R)->asReturnedValue();
}

ReturnedValue ArrayPrototype::method_concat(const FunctionObject *b, const Value *that, const Value *argv, int argc)
{
    Scope scope(b);
    ScopedObject thisObject(scope, that->toObject(scope.engine));
    if (!thisObject)
        RETURN_UNDEFINED();

    ScopedArrayObject result(scope, scope.engine->newArrayObject());

    ScopedArrayObject elt(scope);
    ScopedObject eltAsObj(scope);
    ScopedValue entry(scope);
    for (int i = -1; i < argc; ++i) {
        const Value *v = i == -1 ? thisObject.getPointer() : argv + i;
        eltAsObj = *v;
        elt = *v;
        if (elt) {
            uint n = elt->getLength();
            uint newLen = ArrayData::append(result, elt, n);
            result->setArrayLengthUnchecked(newLen);
        } else if (eltAsObj && eltAsObj->isConcatSpreadable()) {
            const uint startIndex = result->getLength();
            const uint len = eltAsObj->getLength();
            if (scope.hasException())
                return Encode::undefined();

            for (uint i = 0; i < len; ++i) {
                bool hasProperty = false;
                entry = eltAsObj->get(i, &hasProperty);
                if (hasProperty) {
                    if (!result->put(startIndex + i, entry))
                        return scope.engine->throwTypeError();
                }
            }
        } else if (eltAsObj && eltAsObj->isArrayLike()) {
            const uint startIndex = result->getLength();
            for (int i = 0, len = eltAsObj->getLength(); i < len; ++i) {
                entry = eltAsObj->get(i);
                // spec says not to throw if this fails
                result->put(startIndex + i, entry);
            }
        } else {
            result->arraySet(result->getLength(), *v);
        }
    }

    return result.asReturnedValue();
}

ReturnedValue ArrayPrototype::method_copyWithin(const FunctionObject *b, const Value *thisObject, const Value *argv, int argc)
{
    Scope scope(b);
    ScopedObject instance(scope, thisObject->toObject(scope.engine));
    if (!instance)
        RETURN_UNDEFINED();

    double len = instance->getLength();
    double target = argv[0].toInteger();
    double start = argc > 1 ? argv[1].toInteger() : 0;
    double end = len;

    if (argc > 2 && !argv[2].isUndefined()) {
        end = argv[2].toInteger();
    }

    double relativeTarget = target;
    double relativeStart = start;
    double relativeEnd = end;
    double from = 0;
    double to = 0;

    if (relativeTarget < 0) {
        to = std::max(len+relativeTarget, 0.0);
    } else {
        to = std::min(relativeTarget, len);
    }
    if (relativeStart < 0) {
        from = std::max(len+relativeStart, 0.0);
    } else {
        from = std::min(relativeStart, len);
    }

    double fin = 0;
    if (relativeEnd < 0) {
        fin = std::max(len+relativeEnd, 0.0);
    } else {
        fin = std::min(relativeEnd, len);
    }
    double count = std::min(fin-from, len-to);
    double direction = 1;
    if (from < to && to < from+count) {
        direction = -1;
        from = from + count - 1;
        to = to + count - 1;
    }

    while (count > 0) {
        bool fromPresent = false;
        ScopedValue fromVal(scope, instance->get(from, &fromPresent));

        if (fromPresent) {
            instance->setIndexed(to, fromVal, QV4::Object::DoThrowOnRejection);
            CHECK_EXCEPTION();
        } else {
            bool didDelete = instance->deleteProperty(PropertyKey::fromArrayIndex(to));
            CHECK_EXCEPTION();
            if (!didDelete) {
                return scope.engine->throwTypeError();
            }
        }

        from = from + direction;
        to = to + direction;
        count = count - 1;
    }

    return instance.asReturnedValue();
}

ReturnedValue ArrayPrototype::method_entries(const FunctionObject *b, const Value *thisObject, const Value *, int)
{
    Scope scope(b);
    ScopedObject O(scope, thisObject->toObject(scope.engine));
    if (!O)
        RETURN_UNDEFINED();

    Scoped<ArrayIteratorObject> ao(scope, scope.engine->newArrayIteratorObject(O));
    ao->d()->iterationKind = IteratorKind::KeyValueIteratorKind;
    return ao->asReturnedValue();
}

ReturnedValue ArrayPrototype::method_find(const FunctionObject *b, const Value *thisObject, const Value *argv, int argc)
{
    Scope scope(b);
    ScopedObject instance(scope, thisObject->toObject(scope.engine));
    if (!instance)
        RETURN_UNDEFINED();

    uint len = instance->getLength();

    if (!argc || !argv[0].isFunctionObject())
        THROW_TYPE_ERROR();
    const FunctionObject *callback = static_cast<const FunctionObject *>(argv);

    ScopedValue result(scope);
    Value *arguments = scope.alloc(3);

    ScopedValue that(scope, argc > 1 ? argv[1] : Value::undefinedValue());

    for (uint k = 0; k < len; ++k) {
        arguments[0] = instance->get(k);
        CHECK_EXCEPTION();

        arguments[1] = Value::fromDouble(k);
        arguments[2] = instance;
        result = callback->call(that, arguments, 3);

        CHECK_EXCEPTION();
        if (result->toBoolean())
            return arguments[0].asReturnedValue();
    }

    RETURN_UNDEFINED();
}

ReturnedValue ArrayPrototype::method_findIndex(const FunctionObject *b, const Value *thisObject, const Value *argv, int argc)
{
    Scope scope(b);
    ScopedObject instance(scope, thisObject->toObject(scope.engine));
    if (!instance)
        RETURN_UNDEFINED();

    uint len = instance->getLength();

    if (!argc || !argv[0].isFunctionObject())
        THROW_TYPE_ERROR();
    const FunctionObject *callback = static_cast<const FunctionObject *>(argv);

    ScopedValue result(scope);
    Value *arguments = scope.alloc(3);

    ScopedValue that(scope, argc > 1 ? argv[1] : Value::undefinedValue());

    for (uint k = 0; k < len; ++k) {
        arguments[0] = instance->get(k);
        CHECK_EXCEPTION();

        arguments[1] = Value::fromDouble(k);
        arguments[2] = instance;
        result = callback->call(that, arguments, 3);

        CHECK_EXCEPTION();
        if (result->toBoolean())
            return Encode(k);
    }

    return Encode(-1);
}

ReturnedValue ArrayPrototype::method_join(const FunctionObject *functionObject,
                                          const Value *thisObject, const Value *argv, int argc)
{
    Scope scope(functionObject);
    CHECK_STACK_LIMITS(scope.engine)
    ScopedObject instance(scope, thisObject->toObject(scope.engine));

    if (!instance)
        return Encode(scope.engine->newString());

    // We cannot optimize the resolution of the argument away in case of length == 0
    // It may have side effects.
    ScopedValue argument(scope, argc ? argv[0] : Value::undefinedValue());
    const QString separator = argument->isUndefined()
            ? QStringLiteral(",")
            : argument->toQString();

    ScopedValue scopedLength(scope, instance->get(scope.engine->id_length()));
    const quint32 genericLength = scopedLength->isUndefined() ? 0 : scopedLength->toUInt32();
    if (!genericLength)
        return Encode(scope.engine->newString());

    QString result;
    if (auto *arrayObject = instance->as<ArrayObject>()) {
        ScopedValue entry(scope);
        const qint64 arrayLength = arrayObject->getLength();
        Q_ASSERT(arrayLength >= 0);
        Q_ASSERT(arrayLength <= std::numeric_limits<quint32>::max());
        for (quint32 i = 0; i < quint32(arrayLength); ++i) {
            if (i)
                result += separator;

            entry = arrayObject->get(i);
            CHECK_EXCEPTION();
            if (!entry->isNullOrUndefined())
                result += entry->toQString();
        }
    } else {
        ScopedString name(scope, scope.engine->newString(QStringLiteral("0")));
        ScopedValue value(scope, instance->get(name));
        CHECK_EXCEPTION();

        if (!value->isNullOrUndefined())
            result = value->toQString();

        for (quint32 i = 1; i < genericLength; ++i) {
            result += separator;

            name = Value::fromDouble(i).toString(scope.engine);
            value = instance->get(name);
            CHECK_EXCEPTION();

            if (!value->isNullOrUndefined())
                result += value->toQString();
        }
    }

    return Encode(scope.engine->newString(result));
}

ReturnedValue ArrayPrototype::method_pop(const FunctionObject *b, const Value *thisObject, const Value *, int)
{
    Scope scope(b);
    ScopedObject instance(scope, thisObject->toObject(scope.engine));
    if (!instance)
        RETURN_UNDEFINED();

    uint len = instance->getLength();

    if (!len) {
        if (!instance->isArrayObject())
            instance->put(scope.engine->id_length(), ScopedValue(scope, Value::fromInt32(0)));
        RETURN_UNDEFINED();
    }

    ScopedValue result(scope, instance->get(len - 1));
    CHECK_EXCEPTION();

    if (!instance->deleteProperty(PropertyKey::fromArrayIndex(len - 1)))
        return scope.engine->throwTypeError();

    if (instance->isArrayObject())
        instance->setArrayLength(len - 1);
    else {
        if (!instance->put(scope.engine->id_length(), ScopedValue(scope, Value::fromDouble(len - 1))))
            return scope.engine->throwTypeError();
    }
    return result->asReturnedValue();
}

ReturnedValue ArrayPrototype::method_push(const FunctionObject *b, const Value *thisObject, const Value *argv, int argc)
{
    Scope scope(b);
    ScopedObject instance(scope, thisObject->toObject(scope.engine));
    if (!instance)
        RETURN_UNDEFINED();

    instance->arrayCreate();
    Q_ASSERT(instance->arrayData());

    qint64 len = instance->getLength();

    if (len + quint64(argc) >= UINT_MAX) {
        // ughh... this goes beyond UINT_MAX
        double l = len;
        ScopedString s(scope);
        for (int i = 0, ei = argc; i < ei; ++i) {
            s = Value::fromDouble(l + i).toString(scope.engine);
            if (!instance->put(s, argv[i]))
                return scope.engine->throwTypeError();
        }
        double newLen = l + argc;
        if (!instance->isArrayObject()) {
            if (!instance->put(scope.engine->id_length(), ScopedValue(scope, Value::fromDouble(newLen))))
                return scope.engine->throwTypeError();
        } else {
            ScopedString str(scope, scope.engine->newString(QStringLiteral("Array.prototype.push: Overflow")));
            return scope.engine->throwRangeError(str);
        }
        return Encode(newLen);
    }

    if (!argc)
        ;
    else if (!instance->protoHasArray() && instance->arrayData()->length() <= len && instance->arrayData()->type == Heap::ArrayData::Simple) {
        instance->arrayData()->vtable()->putArray(instance, len, argv, argc);
        len = instance->arrayData()->length();
    } else {
        for (int i = 0, ei = argc; i < ei; ++i) {
            if (!instance->put(len + i, argv[i]))
                return scope.engine->throwTypeError();
        }
        len += argc;
    }
    if (instance->isArrayObject())
        instance->setArrayLengthUnchecked(len);
    else {
        if (!instance->put(scope.engine->id_length(), ScopedValue(scope, Value::fromDouble(len))))
            return scope.engine->throwTypeError();
    }

    return Encode(uint(len));
}

ReturnedValue ArrayPrototype::method_reverse(const FunctionObject *b, const Value *thisObject, const Value *, int)
{
    Scope scope(b);
    ScopedObject instance(scope, thisObject->toObject(scope.engine));
    if (!instance)
        RETURN_UNDEFINED();

    qint64 length = instance->getLength();
    // ### FIXME
    if (length >= UINT_MAX)
        return scope.engine->throwRangeError(QLatin1String("Array.prototype.reverse: Length out of range."));

    int lo = 0, hi = length - 1;

    ScopedValue lval(scope);
    ScopedValue hval(scope);
    for (; lo < hi; ++lo, --hi) {
        bool loExists, hiExists;
        lval = instance->get(lo, &loExists);
        hval = instance->get(hi, &hiExists);
        CHECK_EXCEPTION();
        bool ok;
        if (hiExists)
            ok = instance->put(lo, hval);
        else
            ok = instance->deleteProperty(PropertyKey::fromArrayIndex(lo));
        if (ok) {
            if (loExists)
                ok = instance->put(hi, lval);
            else
                ok = instance->deleteProperty(PropertyKey::fromArrayIndex(hi));
        }
        if (!ok)
            return scope.engine->throwTypeError();
    }
    return instance->asReturnedValue();
}

ReturnedValue ArrayPrototype::method_shift(const FunctionObject *b, const Value *thisObject, const Value *, int)
{
    Scope scope(b);
    ScopedObject instance(scope, thisObject->toObject(scope.engine));
    if (!instance)
        RETURN_UNDEFINED();

    instance->arrayCreate();
    Q_ASSERT(instance->arrayData());

    uint len = instance->getLength();

    if (!len) {
        if (!instance->isArrayObject())
            if (!instance->put(scope.engine->id_length(), ScopedValue(scope, Value::fromInt32(0))))
                return scope.engine->throwTypeError();
        RETURN_UNDEFINED();
    }

    ScopedValue result(scope);
    if (!instance->protoHasArray() && !instance->arrayData()->attrs && instance->arrayData()->length() <= len && instance->arrayData()->type != Heap::ArrayData::Custom) {
        result = instance->arrayData()->vtable()->pop_front(instance);
    } else {
        result = instance->get(uint(0));
        CHECK_EXCEPTION();
        ScopedValue v(scope);
        // do it the slow way
        for (uint k = 1; k < len; ++k) {
            bool exists;
            v = instance->get(k, &exists);
            CHECK_EXCEPTION();
            bool ok;
            if (exists)
                ok = instance->put(k - 1, v);
            else
                ok = instance->deleteProperty(PropertyKey::fromArrayIndex(k - 1));
            if (!ok)
                return scope.engine->throwTypeError();
        }
        bool ok = instance->deleteProperty(PropertyKey::fromArrayIndex(len - 1));
        if (!ok)
            return scope.engine->throwTypeError();
    }

    if (instance->isArrayObject())
        instance->setArrayLengthUnchecked(len - 1);
    else {
        bool ok = instance->put(scope.engine->id_length(), ScopedValue(scope, Value::fromDouble(len - 1)));
        if (!ok)
            return scope.engine->throwTypeError();
    }

    return result->asReturnedValue();
}

ReturnedValue ArrayPrototype::method_slice(const FunctionObject *b, const Value *thisObject, const Value *argv, int argc)
{
    Scope scope(b);
    ScopedObject o(scope, thisObject->toObject(scope.engine));
    if (!o)
        RETURN_UNDEFINED();

    ScopedArrayObject result(scope, scope.engine->newArrayObject());
    uint len = o->getLength();
    double s = (argc ? argv[0] : Value::undefinedValue()).toInteger();
    uint start;
    if (s < 0)
        start = (uint)qMax(len + s, 0.);
    else if (s > len)
        start = len;
    else
        start = (uint) s;
    uint end = len;
    if (argc > 1 && !argv[1].isUndefined()) {
        double e = argv[1].toInteger();
        if (e < 0)
            end = (uint)qMax(len + e, 0.);
        else if (e > len)
            end = len;
        else
            end = (uint) e;
    }

    ScopedValue v(scope);
    uint n = 0;
    for (uint i = start; i < end; ++i) {
        bool exists;
        v = o->get(i, &exists);
        CHECK_EXCEPTION();
        if (exists)
            result->arraySet(n, v);
        ++n;
    }
    return result->asReturnedValue();
}

ReturnedValue ArrayPrototype::method_sort(const FunctionObject *b, const Value *thisObject, const Value *argv, int argc)
{
    // Based on https://tc39.es/ecma262/#sec-array.prototype.sort

    Scope scope(b);

    ScopedValue comparefn(scope, argc ? argv[0] : Value::undefinedValue());

    // 1. If comparefn is not undefined and IsCallable(comparefn) is false, throw a TypeError exception.
    if (!comparefn->isUndefined() && !comparefn->isFunctionObject())
        return scope.engine->throwTypeError(QStringLiteral("The provided comparison function is not callable."));

    // 2. Let obj be ? ToObject(this value).
    ScopedObject instance(scope, thisObject->toObject(scope.engine));
    if (!instance)
        RETURN_UNDEFINED();

    // 3. Let len be ? LengthOfArrayLike(obj).
    uint len = instance->getLength();

    if (instance->arrayData() && instance->arrayData()->length()) {
        ArrayData::sort(scope.engine, instance, comparefn, len);
    } else {
        // Generic implementation that does not require a populated
        // ArrayData, this is used, for example, by `Sequences` which
        // store their data in a different way.

        // 5. Let sortedList be ? SortIndexedProperties(obj, len, SortCompare, skip-holes)
        Value* sorted = scope.alloc(scope.engine->safeForAllocLength(len));
        CHECK_EXCEPTION();

        uint written = 0;
        for (uint index = 0; index < len; ++index) {
            bool hasProperty = false;
            auto element = instance->get(index, &hasProperty);

            if (hasProperty) {
                sorted[written] = element;
                ++written;
            }
        }

        std::stable_sort(sorted, sorted + written, ArrayElementLessThan(scope.engine, comparefn));

        // [...]
        // 8. Repeat, while j < itemCount,
        //      a. Perform ? Set(obj, ! ToString(𝔽(j)), sortedList[j], true).
        // [...]
        for (uint index = 0; index < written; ++index) {
            instance->setIndexed(index, sorted[index], QV4::Object::DoThrowOnRejection);
            CHECK_EXCEPTION();
        }

        // [...]
        // 10. Repeat, while j < len,
        //      a. Perform ? DeletePropertyOrThrow(obj, ! ToString(𝔽(j))).
        // [...]
        while (written < len) {
            if (!instance->deleteProperty(PropertyKey::fromArrayIndex(written)))
                return scope.engine->throwTypeError();
            ++written;
        }
    }

    // 11. Return obj
    return thisObject->asReturnedValue();
}

ReturnedValue ArrayPrototype::method_splice(const FunctionObject *b, const Value *thisObject, const Value *argv, int argc)
{
    Scope scope(b);
    ScopedObject instance(scope, thisObject->toObject(scope.engine));
    if (!instance)
        RETURN_UNDEFINED();

    qint64 len = instance->getLength();

    double rs = (argc ? argv[0] : Value::undefinedValue()).toInteger();
    qint64 start;
    if (rs < 0)
        start = static_cast<qint64>(qMax(0., len + rs));
    else
        start = static_cast<qint64>(qMin(rs, static_cast<double>(len)));

    qint64 deleteCount = 0;
    qint64 itemCount = 0;
    if (argc == 1) {
        deleteCount = len - start;
    } else if (argc > 1){
        itemCount = argc - 2;
        double dc = argv[1].toInteger();
        deleteCount = static_cast<qint64>(qMin(qMax(dc, 0.), double(len - start)));
    }

    if (len + itemCount - deleteCount > /*(static_cast<qint64>(1) << 53) - 1*/ UINT_MAX - 1)
        return scope.engine->throwTypeError();
    if (deleteCount > /*(static_cast<qint64>(1) << 53) - 1*/ UINT_MAX - 1)
        return scope.engine->throwRangeError(QString::fromLatin1("Array length out of range."));

    ScopedArrayObject newArray(scope, scope.engine->newArrayObject());
    newArray->arrayReserve(deleteCount);
    ScopedValue v(scope);
    for (uint i = 0; i < deleteCount; ++i) {
        bool exists;
        v = instance->get(start + i, &exists);
        CHECK_EXCEPTION();
        if (exists)
            newArray->arrayPut(i, v);
    }
    newArray->setArrayLengthUnchecked(deleteCount);


    if (itemCount < deleteCount) {
        for (uint k = start; k < len - deleteCount; ++k) {
            bool exists;
            v = instance->get(k + deleteCount, &exists);
            CHECK_EXCEPTION();
            bool ok;
            if (exists)
                ok = instance->put(k + itemCount, v);
            else
                ok = instance->deleteProperty(PropertyKey::fromArrayIndex(k + itemCount));
            if (!ok)
                return scope.engine->throwTypeError();
        }
        for (uint k = len; k > len - deleteCount + itemCount; --k) {
            if (!instance->deleteProperty(PropertyKey::fromArrayIndex(k - 1)))
                return scope.engine->throwTypeError();
        }
    } else if (itemCount > deleteCount) {
        uint k = len - deleteCount;
        while (k > start) {
            bool exists;
            v = instance->get(k + deleteCount - 1, &exists);
            CHECK_EXCEPTION();
            bool ok;
            if (exists)
                ok = instance->put(k + itemCount - 1, v);
            else
                ok = instance->deleteProperty(PropertyKey::fromArrayIndex(k + itemCount - 1));
            if (!ok)
                return scope.engine->throwTypeError();
            --k;
        }
    }

    for (uint i = 0; i < itemCount; ++i)
        instance->put(start + i, argv[i + 2]);

    if (!instance->put(scope.engine->id_length(), ScopedValue(scope, Value::fromDouble(len - deleteCount + itemCount))))
        return scope.engine->throwTypeError();

    return newArray->asReturnedValue();
}

ReturnedValue ArrayPrototype::method_unshift(const FunctionObject *b, const Value *thisObject, const Value *argv, int argc)
{
    Scope scope(b);
    ScopedObject instance(scope, thisObject->toObject(scope.engine));
    if (!instance)
        RETURN_UNDEFINED();

    instance->arrayCreate();
    Q_ASSERT(instance->arrayData());

    uint len = instance->getLength();

    if (!instance->protoHasArray() && !instance->arrayData()->attrs && instance->arrayData()->length() <= len &&
        instance->arrayData()->type != Heap::ArrayData::Custom) {
        instance->arrayData()->vtable()->push_front(instance, argv, argc);
    } else {
        ScopedValue v(scope);
        for (uint k = len; k > 0; --k) {
            bool exists;
            v = instance->get(k - 1, &exists);
            bool ok;
            if (exists)
                ok = instance->put(k + argc - 1, v);
            else
                ok = instance->deleteProperty(PropertyKey::fromArrayIndex(k + argc - 1));
            if (!ok)
                return scope.engine->throwTypeError();
        }
        for (int i = 0, ei = argc; i < ei; ++i) {
            bool ok = instance->put(i, argv[i]);
            if (!ok)
                return scope.engine->throwTypeError();
        }
    }

    uint newLen = len + argc;
    if (instance->isArrayObject())
        instance->setArrayLengthUnchecked(newLen);
    else {
        if (!instance->put(scope.engine->id_length(), ScopedValue(scope, Value::fromDouble(newLen))))
            return scope.engine->throwTypeError();
    }

    return Encode(newLen);
}

ReturnedValue ArrayPrototype::method_includes(const FunctionObject *b, const Value *thisObject, const Value *argv, int argc)
{
    Scope scope(b);
    ScopedObject instance(scope, thisObject->toObject(scope.engine));
    if (!instance)
        RETURN_UNDEFINED();

    qint64 len = instance->getLength();
    if (len == 0) {
        return Encode(false);
    }

    double n = 0;
    if (argc > 1 && !argv[1].isUndefined()) {
        n = argv[1].toInteger();
    }

    double k = 0;
    if (n >= 0) {
        k = n;
    } else {
        k = len + n;
        if (k < 0) {
            k = 0;
        }
    }

    ScopedValue val(scope);
    while (k < len) {
        val = instance->get(k);
        if (val->sameValueZero(argv[0])) {
            return Encode(true);
        }
        k++;
    }

    return Encode(false);
}

ReturnedValue ArrayPrototype::method_indexOf(const FunctionObject *b, const Value *thisObject, const Value *argv, int argc)
{
    Scope scope(b);
    ScopedObject instance(scope, thisObject->toObject(scope.engine));
    if (!instance)
        RETURN_UNDEFINED();

    uint len = instance->getLength();
    if (!len)
        return Encode(-1);

    ScopedValue searchValue(scope, argc ? argv[0] : Value::undefinedValue());
    uint fromIndex = 0;

    if (argc >= 2) {
        double f = argv[1].toInteger();
        CHECK_EXCEPTION();
        if (f >= len)
            return Encode(-1);
        if (f < 0)
            f = qMax(len + f, 0.);
        fromIndex = (uint) f;
    }

    if (instance->isStringObject()) {
        ScopedValue v(scope);
        for (uint k = fromIndex; k < len; ++k) {
            bool exists;
            v = instance->get(k, &exists);
            if (exists && RuntimeHelpers::strictEqual(v, searchValue))
                return Encode(k);
        }
        return Encode(-1);
    }

    ScopedValue value(scope);

    if (ArgumentsObject::isNonStrictArgumentsObject(instance) ||
        (instance->arrayType() >= Heap::ArrayData::Sparse) || instance->protoHasArray()) {
        // lets be safe and slow
        for (uint i = fromIndex; i < len; ++i) {
            bool exists;
            value = instance->get(i, &exists);
            CHECK_EXCEPTION();
            if (exists && RuntimeHelpers::strictEqual(value, searchValue))
                return Encode(i);
        }
    } else if (!instance->arrayData()) {
        return Encode(-1);
    } else {
        Q_ASSERT(instance->arrayType() == Heap::ArrayData::Simple);
        Heap::SimpleArrayData *sa = instance->d()->arrayData.cast<Heap::SimpleArrayData>();
        if (len > sa->values.size)
            len = sa->values.size;
        uint idx = fromIndex;
        while (idx < len) {
            value = sa->data(idx);
            CHECK_EXCEPTION();
            if (RuntimeHelpers::strictEqual(value, searchValue))
                return Encode(idx);
            ++idx;
        }
    }
    return Encode(-1);
}

ReturnedValue ArrayPrototype::method_keys(const FunctionObject *f, const Value *thisObject, const Value *, int)
{
    Scope scope(f);
    ScopedObject O(scope, thisObject->toObject(scope.engine));
    if (!O)
        RETURN_UNDEFINED();

    Scoped<ArrayIteratorObject> ao(scope, scope.engine->newArrayIteratorObject(O));
    ao->d()->iterationKind = IteratorKind::KeyIteratorKind;
    return ao->asReturnedValue();
}

ReturnedValue ArrayPrototype::method_lastIndexOf(const FunctionObject *b, const Value *thisObject, const Value *argv, int argc)
{
    Scope scope(b);
    ScopedObject instance(scope, thisObject->toObject(scope.engine));
    if (!instance)
        RETURN_UNDEFINED();

    uint len = instance->getLength();
    if (!len)
        return Encode(-1);

    ScopedValue searchValue(scope);
    uint fromIndex = len;

    if (argc >= 1)
        searchValue = argv[0];
    else
        searchValue = Value::undefinedValue();

    if (argc >= 2) {
        double f = argv[1].toInteger();
        CHECK_EXCEPTION();
        if (f > 0)
            f = qMin(f, (double)(len - 1));
        else if (f < 0) {
            f = len + f;
            if (f < 0)
                return Encode(-1);
        }
        fromIndex = (uint) f + 1;
    }

    ScopedValue v(scope);
    for (uint k = fromIndex; k > 0;) {
        --k;
        bool exists;
        v = instance->get(k, &exists);
        CHECK_EXCEPTION();
        if (exists && RuntimeHelpers::strictEqual(v, searchValue))
            return Encode(k);
    }
    return Encode(-1);
}

ReturnedValue ArrayPrototype::method_every(const FunctionObject *b, const Value *thisObject, const Value *argv, int argc)
{
    Scope scope(b);
    ScopedObject instance(scope, thisObject->toObject(scope.engine));
    if (!instance)
        RETURN_UNDEFINED();

    uint len = instance->getLength();

    if (!argc || !argv->isFunctionObject())
        THROW_TYPE_ERROR();
    const FunctionObject *callback = static_cast<const FunctionObject *>(argv);

    ScopedValue that(scope, argc > 1 ? argv[1] : Value::undefinedValue());
    ScopedValue r(scope);
    Value *arguments = scope.alloc(3);

    bool ok = true;
    for (uint k = 0; ok && k < len; ++k) {
        bool exists;
        arguments[0] = instance->get(k, &exists);
        if (!exists)
            continue;

        arguments[1] = Value::fromDouble(k);
        arguments[2] = instance;
        r = callback->call(that, arguments, 3);
        CHECK_EXCEPTION();
        ok = r->toBoolean();
    }
    return Encode(ok);
}

ReturnedValue ArrayPrototype::method_fill(const FunctionObject *b, const Value *thisObject, const Value *argv, int argc)
{
    Scope scope(b);
    ScopedObject instance(scope, thisObject->toObject(scope.engine));
    if (!instance)
        RETURN_UNDEFINED();

    const qsizetype len = instance->getLength();
    Q_ASSERT(len >= 0);

    const qsizetype relativeStart = argc > 1 ? argv[1].toInteger() : 0;
    qsizetype relativeEnd = len;
    if (argc > 2 && !argv[2].isUndefined())
        relativeEnd = argv[2].toInteger();

    qsizetype k = 0;
    qsizetype fin = 0;

    if (relativeStart < 0) {
        if (relativeStart > -len)
            k = std::max(len + relativeStart, qsizetype(0));
    } else {
        k = std::min(relativeStart, len);
    }
    Q_ASSERT(k >= 0);

    if (relativeEnd < 0) {
        if (relativeEnd > -len)
            fin = std::max(len + relativeEnd, qsizetype(0));
    } else {
        fin = std::min(relativeEnd, len);
    }
    Q_ASSERT(fin >= 0);

    if (sizeof(qsizetype) > sizeof(uint) && fin > qsizetype(std::numeric_limits<uint>::max()))
        return scope.engine->throwRangeError(QString::fromLatin1("Array length out of range."));

    for (; k < fin; ++k)
        instance->setIndexed(uint(k), argv[0], QV4::Object::DoThrowOnRejection);

    return instance.asReturnedValue();
}

ReturnedValue ArrayPrototype::method_some(const FunctionObject *b, const Value *thisObject, const Value *argv, int argc)
{
    Scope scope(b);
    ScopedObject instance(scope, thisObject->toObject(scope.engine));
    if (!instance)
        RETURN_UNDEFINED();

    uint len = instance->getLength();

    if (!argc || !argv->isFunctionObject())
        THROW_TYPE_ERROR();
    const FunctionObject *callback = static_cast<const FunctionObject *>(argv);

    ScopedValue that(scope, argc > 1 ? argv[1] : Value::undefinedValue());
    ScopedValue result(scope);
    Value *arguments = scope.alloc(3);

    for (uint k = 0; k < len; ++k) {
        bool exists;
        arguments[0] = instance->get(k, &exists);
        if (!exists)
            continue;

        arguments[1] = Value::fromDouble(k);
        arguments[2] = instance;
        result = callback->call(that, arguments, 3);
        CHECK_EXCEPTION();
        if (result->toBoolean())
            return Encode(true);
    }
    return Encode(false);
}

ReturnedValue ArrayPrototype::method_forEach(const FunctionObject *b, const Value *thisObject, const Value *argv, int argc)
{
    Scope scope(b);
    ScopedObject instance(scope, thisObject->toObject(scope.engine));
    if (!instance)
        RETURN_UNDEFINED();

    uint len = instance->getLength();

    if (!argc || !argv->isFunctionObject())
        THROW_TYPE_ERROR();
    const FunctionObject *callback = static_cast<const FunctionObject *>(argv);

    ScopedValue that(scope, argc > 1 ? argv[1] : Value::undefinedValue());
    Value *arguments = scope.alloc(3);

    for (uint k = 0; k < len; ++k) {
        bool exists;
        arguments[0] = instance->get(k, &exists);
        if (!exists)
            continue;

        arguments[1] = Value::fromDouble(k);
        arguments[2] = instance;
        callback->call(that, arguments, 3);
    }
    RETURN_UNDEFINED();
}

ReturnedValue ArrayPrototype::method_map(const FunctionObject *b, const Value *thisObject, const Value *argv, int argc)
{
    Scope scope(b);
    ScopedObject instance(scope, thisObject->toObject(scope.engine));
    if (!instance)
        RETURN_UNDEFINED();

    qint64 len = instance->getLength();

    if (!argc || !argv->isFunctionObject())
        THROW_TYPE_ERROR();
    const FunctionObject *callback = static_cast<const FunctionObject *>(argv);

    if (len > UINT_MAX - 1)
        return scope.engine->throwRangeError(QString::fromLatin1("Array length out of range."));

    ScopedArrayObject a(scope, scope.engine->newArrayObject());
    a->arrayReserve(len);
    a->setArrayLengthUnchecked(len);

    ScopedValue v(scope);
    ScopedValue mapped(scope);
    ScopedValue that(scope, argc > 1 ? argv[1] : Value::undefinedValue());
    Value *arguments = scope.alloc(3);

    for (uint k = 0; k < len; ++k) {
        bool exists;
        arguments[0] = instance->get(k, &exists);
        if (!exists)
            continue;

        arguments[1] = Value::fromDouble(k);
        arguments[2] = instance;
        mapped = callback->call(that, arguments, 3);
        CHECK_EXCEPTION();
        a->arraySet(k, mapped);
    }
    return a.asReturnedValue();
}

ReturnedValue ArrayPrototype::method_filter(const FunctionObject *b, const Value *thisObject, const Value *argv, int argc)
{
    Scope scope(b);
    ScopedObject instance(scope, thisObject->toObject(scope.engine));
    if (!instance)
        RETURN_UNDEFINED();

    uint len = instance->getLength();

    if (!argc || !argv->isFunctionObject())
        THROW_TYPE_ERROR();
    const FunctionObject *callback = static_cast<const FunctionObject *>(argv);

    ScopedArrayObject a(scope, scope.engine->newArrayObject());
    a->arrayReserve(len);

    ScopedValue selected(scope);
    ScopedValue that(scope, argc > 1 ? argv[1] : Value::undefinedValue());
    Value *arguments = scope.alloc(3);

    uint to = 0;
    for (uint k = 0; k < len; ++k) {
        bool exists;
        arguments[0] = instance->get(k, &exists);
        if (!exists)
            continue;

        arguments[1] = Value::fromDouble(k);
        arguments[2] = instance;
        selected = callback->call(that, arguments, 3);
        CHECK_EXCEPTION();
        if (selected->toBoolean()) {
            a->arraySet(to, arguments[0]);
            ++to;
        }
    }
    return a.asReturnedValue();
}

ReturnedValue ArrayPrototype::method_reduce(const FunctionObject *b, const Value *thisObject, const Value *argv, int argc)
{
    Scope scope(b);
    ScopedObject instance(scope, thisObject->toObject(scope.engine));
    if (!instance)
        RETURN_UNDEFINED();

    uint len = instance->getLength();

    if (!argc || !argv->isFunctionObject())
        THROW_TYPE_ERROR();
    const FunctionObject *callback = static_cast<const FunctionObject *>(argv);

    uint k = 0;
    ScopedValue acc(scope);
    ScopedValue v(scope);

    if (argc > 1) {
        acc = argv[1];
    } else {
        bool kPresent = false;
        while (k < len && !kPresent) {
            v = instance->get(k, &kPresent);
            if (kPresent)
                acc = v;
            ++k;
        }
        if (!kPresent)
            THROW_TYPE_ERROR();
    }

    Value *arguments = scope.alloc(4);

    while (k < len) {
        bool kPresent;
        v = instance->get(k, &kPresent);
        if (kPresent) {
            arguments[0] = acc;
            arguments[1] = v;
            arguments[2] = Value::fromDouble(k);
            arguments[3] = instance;
            acc = callback->call(nullptr, arguments, 4);
            CHECK_EXCEPTION();
        }
        ++k;
    }
    return acc->asReturnedValue();
}

ReturnedValue ArrayPrototype::method_reduceRight(const FunctionObject *b, const Value *thisObject, const Value *argv, int argc)
{
    Scope scope(b);
    ScopedObject instance(scope, thisObject->toObject(scope.engine));
    if (!instance)
        RETURN_UNDEFINED();

    uint len = instance->getLength();

    if (!argc || !argv->isFunctionObject())
        THROW_TYPE_ERROR();
    const FunctionObject *callback = static_cast<const FunctionObject *>(argv);

    if (len == 0) {
        if (argc == 1)
            THROW_TYPE_ERROR();
        return argv[1].asReturnedValue();
    }

    uint k = len;
    ScopedValue acc(scope);
    ScopedValue v(scope);
    if (argc > 1) {
        acc = argv[1];
    } else {
        bool kPresent = false;
        while (k > 0 && !kPresent) {
            v = instance->get(k - 1, &kPresent);
            if (kPresent)
                acc = v;
            --k;
        }
        if (!kPresent)
            THROW_TYPE_ERROR();
    }

    Value *arguments = scope.alloc(4);

    while (k > 0) {
        bool kPresent;
        v = instance->get(k - 1, &kPresent);
        if (kPresent) {
            arguments[0] = acc;
            arguments[1] = v;
            arguments[2] = Value::fromDouble(k - 1);
            arguments[3] = instance;
            acc = callback->call(nullptr, arguments, 4);
            CHECK_EXCEPTION();
        }
        --k;
    }
    return acc->asReturnedValue();
}

ReturnedValue ArrayPrototype::method_values(const FunctionObject *b, const Value *thisObject, const Value *, int)
{
    Scope scope(b);
    ScopedObject O(scope, thisObject->toObject(scope.engine));
    if (!O)
        RETURN_UNDEFINED();

    Scoped<ArrayIteratorObject> ao(scope, scope.engine->newArrayIteratorObject(O));
    ao->d()->iterationKind = IteratorKind::ValueIteratorKind;
    return ao->asReturnedValue();
}

ReturnedValue ArrayPrototype::method_get_species(const FunctionObject *, const Value *thisObject, const Value *, int)
{
    return thisObject->asReturnedValue();
}
