//! This module implements the global `Array` object.
//!
//! The JavaScript `Array` class is a global object that is used in the construction of arrays; which are high-level, list-like objects.
//!
//! More information:
//!  - [ECMAScript reference][spec]
//!  - [MDN documentation][mdn]
//!
//! [spec]: https://tc39.es/ecma262/#sec-array-objects
//! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array

pub mod array_iterator;
#[cfg(test)]
mod tests;

use boa_profiler::Profiler;
use tap::{Conv, Pipe};

use super::JsArgs;
use crate::{
    builtins::array::array_iterator::ArrayIterator,
    builtins::iterable::{if_abrupt_close_iterator, IteratorHint},
    builtins::BuiltIn,
    builtins::Number,
    context::intrinsics::StandardConstructors,
    object::{
        internal_methods::get_prototype_from_constructor, ConstructorBuilder, FunctionBuilder,
        JsFunction, JsObject, ObjectData,
    },
    property::{Attribute, PropertyDescriptor, PropertyNameKind},
    symbol::WellKnownSymbols,
    value::{IntegerOrInfinity, JsValue},
    Context, JsResult, JsString,
};
use std::cmp::{max, min, Ordering};

/// JavaScript `Array` built-in implementation.
#[derive(Debug, Clone, Copy)]
pub(crate) struct Array;

impl BuiltIn for Array {
    const NAME: &'static str = "Array";

    fn init(context: &mut Context) -> Option<JsValue> {
        let _timer = Profiler::global().start_event(Self::NAME, "init");

        let symbol_iterator = WellKnownSymbols::iterator();
        let symbol_unscopables = WellKnownSymbols::unscopables();

        let get_species = FunctionBuilder::native(context, Self::get_species)
            .name("get [Symbol.species]")
            .constructor(false)
            .build();

        let values_function = Self::values_intrinsic(context);
        let unscopables_object = Self::unscopables_intrinsic(context);

        ConstructorBuilder::with_standard_constructor(
            context,
            Self::constructor,
            context.intrinsics().constructors().array().clone(),
        )
        .name(Self::NAME)
        .length(Self::LENGTH)
        .static_accessor(
            WellKnownSymbols::species(),
            Some(get_species),
            None,
            Attribute::CONFIGURABLE,
        )
        .property(
            "length",
            0,
            Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::PERMANENT,
        )
        .property(
            "values",
            values_function.clone(),
            Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
        )
        .property(
            symbol_iterator,
            values_function,
            Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
        )
        .property(
            symbol_unscopables,
            unscopables_object,
            Attribute::READONLY | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
        )
        .method(Self::at, "at", 1)
        .method(Self::concat, "concat", 1)
        .method(Self::push, "push", 1)
        .method(Self::index_of, "indexOf", 1)
        .method(Self::last_index_of, "lastIndexOf", 1)
        .method(Self::includes_value, "includes", 1)
        .method(Self::map, "map", 1)
        .method(Self::fill, "fill", 1)
        .method(Self::for_each, "forEach", 1)
        .method(Self::filter, "filter", 1)
        .method(Self::pop, "pop", 0)
        .method(Self::join, "join", 1)
        .method(Self::to_string, "toString", 0)
        .method(Self::reverse, "reverse", 0)
        .method(Self::shift, "shift", 0)
        .method(Self::unshift, "unshift", 1)
        .method(Self::every, "every", 1)
        .method(Self::find, "find", 1)
        .method(Self::find_index, "findIndex", 1)
        .method(Self::find_last, "findLast", 1)
        .method(Self::find_last_index, "findLastIndex", 1)
        .method(Self::flat, "flat", 0)
        .method(Self::flat_map, "flatMap", 1)
        .method(Self::slice, "slice", 2)
        .method(Self::some, "some", 1)
        .method(Self::sort, "sort", 1)
        .method(Self::splice, "splice", 2)
        .method(Self::reduce, "reduce", 1)
        .method(Self::reduce_right, "reduceRight", 1)
        .method(Self::keys, "keys", 0)
        .method(Self::entries, "entries", 0)
        .method(Self::copy_within, "copyWithin", 2)
        // Static Methods
        .static_method(Self::from, "from", 1)
        .static_method(Self::is_array, "isArray", 1)
        .static_method(Self::of, "of", 0)
        .build()
        .conv::<JsValue>()
        .pipe(Some)
    }
}

impl Array {
    const LENGTH: usize = 1;

    fn constructor(
        new_target: &JsValue,
        args: &[JsValue],
        context: &mut Context,
    ) -> JsResult<JsValue> {
        // If NewTarget is undefined, let newTarget be the active function object; else let newTarget be NewTarget.
        // 2. Let proto be ? GetPrototypeFromConstructor(newTarget, "%Array.prototype%").
        let prototype =
            get_prototype_from_constructor(new_target, StandardConstructors::array, context)?;

        // 3. Let numberOfArgs be the number of elements in values.
        let number_of_args = args.len();

        // 4. If numberOfArgs = 0, then
        if number_of_args == 0 {
            // 4.a. Return ! ArrayCreate(0, proto).
            Ok(Self::array_create(0, Some(prototype), context)
                .expect("this ArrayCreate call must not fail")
                .into())
        // 5. Else if numberOfArgs = 1, then
        } else if number_of_args == 1 {
            // a. Let len be values[0].
            let len = &args[0];
            // b. Let array be ! ArrayCreate(0, proto).
            let array = Self::array_create(0, Some(prototype), context)
                .expect("this ArrayCreate call must not fail");
            // c. If Type(len) is not Number, then
            #[allow(clippy::if_not_else)]
            let int_len = if !len.is_number() {
                // i. Perform ! CreateDataPropertyOrThrow(array, "0", len).
                array
                    .create_data_property_or_throw(0, len, context)
                    .expect("this CreateDataPropertyOrThrow call must not fail");
                // ii. Let intLen be 1𝔽.
                1
            // d. Else,
            } else {
                // i. Let intLen be ! ToUint32(len).
                let int_len = len
                    .to_u32(context)
                    .expect("this ToUint32 call must not fail");
                // ii. If SameValueZero(intLen, len) is false, throw a RangeError exception.
                if !JsValue::same_value_zero(&int_len.into(), len) {
                    return context.throw_range_error("invalid array length");
                }
                int_len
            };
            // e. Perform ! Set(array, "length", intLen, true).
            array
                .set("length", int_len, true, context)
                .expect("this Set call must not fail");
            // f. Return array.
            Ok(array.into())
        // 6. Else,
        } else {
            // 6.a. Assert: numberOfArgs ≥ 2.
            debug_assert!(number_of_args >= 2);

            // b. Let array be ? ArrayCreate(numberOfArgs, proto).
            let array = Self::array_create(number_of_args, Some(prototype), context)?;
            // c. Let k be 0.
            // d. Repeat, while k < numberOfArgs,
            for (i, item) in args.iter().cloned().enumerate() {
                // i. Let Pk be ! ToString(𝔽(k)).
                // ii. Let itemK be values[k].
                // iii. Perform ! CreateDataPropertyOrThrow(array, Pk, itemK).
                array
                    .create_data_property_or_throw(i, item, context)
                    .expect("this CreateDataPropertyOrThrow must not fail");
                // iv. Set k to k + 1.
            }
            // e. Assert: The mathematical value of array's "length" property is numberOfArgs.
            // f. Return array.
            Ok(array.into())
        }
    }

    /// Utility for constructing `Array` objects.
    ///
    /// More information:
    ///  - [ECMAScript reference][spec]
    ///
    /// [spec]: https://tc39.es/ecma262/#sec-arraycreate
    pub(crate) fn array_create(
        length: usize,
        prototype: Option<JsObject>,
        context: &mut Context,
    ) -> JsResult<JsObject> {
        // 1. If length > 2^32 - 1, throw a RangeError exception.
        if length > u32::MAX as usize { //2usize.pow(32) - 1 {
            return context.throw_range_error("array exceeded max size");
        }
        // 7. Return A.
        // 2. If proto is not present, set proto to %Array.prototype%.
        // 3. Let A be ! MakeBasicObject(« [[Prototype]], [[Extensible]] »).
        // 4. Set A.[[Prototype]] to proto.
        // 5. Set A.[[DefineOwnProperty]] as specified in 10.4.2.1.
        let prototype = match prototype {
            Some(prototype) => prototype,
            None => context.intrinsics().constructors().array().prototype(),
        };
        let array = JsObject::from_proto_and_data(prototype, ObjectData::array());

        // 6. Perform ! OrdinaryDefineOwnProperty(A, "length", PropertyDescriptor { [[Value]]: 𝔽(length), [[Writable]]: true, [[Enumerable]]: false, [[Configurable]]: false }).
        crate::object::internal_methods::ordinary_define_own_property(
            &array,
            "length".into(),
            PropertyDescriptor::builder()
                .value(length)
                .writable(true)
                .enumerable(false)
                .configurable(false)
                .build(),
            context,
        )?;

        Ok(array)
    }

    /// Utility for constructing `Array` objects from an iterator of `JsValue`s.
    ///
    /// More information:
    ///  - [ECMAScript reference][spec]
    ///
    /// [spec]: https://tc39.es/ecma262/#sec-createarrayfromlist
    pub(crate) fn create_array_from_list<I>(elements: I, context: &mut Context) -> JsObject
    where
        I: IntoIterator<Item = JsValue>,
    {
        // 1. Assert: elements is a List whose elements are all ECMAScript language values.
        // 2. Let array be ! ArrayCreate(0).
        let array = Self::array_create(0, None, context)
            .expect("creating an empty array with the default prototype must not fail");

        // 3. Let n be 0.
        // 4. For each element e of elements, do
        //     a. Perform ! CreateDataPropertyOrThrow(array, ! ToString(𝔽(n)), e).
        //     b. Set n to n + 1.
        //
        // NOTE: This deviates from the spec, but it should have the same behaviour.
        let elements: Vec<_> = elements.into_iter().collect();
        let length = elements.len();
        array
            .borrow_mut()
            .properties_mut()
            .override_indexed_properties(elements);
        array
            .set("length", length, true, context)
            .expect("Should not fail");

        // 5. Return array.
        array
    }

    /// Utility function for concatenating array objects.
    ///
    /// Returns a Boolean valued property that if `true` indicates that
    /// an object should be flattened to its array elements
    /// by `Array.prototype.concat`.
    fn is_concat_spreadable(o: &JsValue, context: &mut Context) -> JsResult<bool> {
        // 1. If Type(O) is not Object, return false.
        let o = if let Some(o) = o.as_object() {
            o
        } else {
            return Ok(false);
        };

        // 2. Let spreadable be ? Get(O, @@isConcatSpreadable).
        let spreadable = o.get(WellKnownSymbols::is_concat_spreadable(), context)?;

        // 3. If spreadable is not undefined, return ! ToBoolean(spreadable).
        if !spreadable.is_undefined() {
            return Ok(spreadable.to_boolean());
        }

        // 4. Return ? IsArray(O).
        o.is_array_abstract(context)
    }

    /// `get Array [ @@species ]`
    ///
    /// The `Array [ @@species ]` accessor property returns the Array constructor.
    ///
    /// More information:
    ///  - [ECMAScript reference][spec]
    ///  - [MDN documentation][mdn]
    ///
    /// [spec]: https://tc39.es/ecma262/#sec-get-array-@@species
    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/@@species
    #[allow(clippy::unnecessary_wraps)]
    fn get_species(this: &JsValue, _: &[JsValue], _: &mut Context) -> JsResult<JsValue> {
        // 1. Return the this value.
        Ok(this.clone())
    }

    /// Utility function used to specify the creation of a new Array object using a constructor
    /// function that is derived from `original_array`.
    ///
    /// see: <https://tc39.es/ecma262/#sec-arrayspeciescreate>
    pub(crate) fn array_species_create(
        original_array: &JsObject,
        length: usize,
        context: &mut Context,
    ) -> JsResult<JsObject> {
        // 1. Let isArray be ? IsArray(originalArray).
        // 2. If isArray is false, return ? ArrayCreate(length).
        if !original_array.is_array_abstract(context)? {
            return Self::array_create(length, None, context);
        }
        // 3. Let C be ? Get(originalArray, "constructor").
        let c = original_array.get("constructor", context)?;

        // 4. If IsConstructor(C) is true, then
        //     a. Let thisRealm be the current Realm Record.
        //     b. Let realmC be ? GetFunctionRealm(C).
        //     c. If thisRealm and realmC are not the same Realm Record, then
        //         i. If SameValue(C, realmC.[[Intrinsics]].[[%Array%]]) is true, set C to undefined.
        // TODO: Step 4 is ignored, as there are no different realms for now

        // 5. If Type(C) is Object, then
        let c = if let Some(c) = c.as_object() {
            // 5.a. Set C to ? Get(C, @@species).
            let c = c.get(WellKnownSymbols::species(), context)?;
            // 5.b. If C is null, set C to undefined.
            if c.is_null_or_undefined() {
                JsValue::undefined()
            } else {
                c
            }
        } else {
            c
        };

        // 6. If C is undefined, return ? ArrayCreate(length).
        if c.is_undefined() {
            return Self::array_create(length, None, context);
        }

        // 7. If IsConstructor(C) is false, throw a TypeError exception.
        if let Some(c) = c.as_constructor() {
            // 8. Return ? Construct(C, « 𝔽(length) »).
            c.construct(&[JsValue::new(length)], Some(c), context)
        } else {
            context.throw_type_error("Symbol.species must be a constructor")
        }
    }

    /// `Array.from(arrayLike)`
    ///
    /// The Array.from() static method creates a new,
    /// shallow-copied Array instance from an array-like or iterable object.
    ///
    /// More information:
    ///  - [ECMAScript reference][spec]
    ///  - [MDN documentation][mdn]
    ///
    /// [spec]: https://tc39.es/ecma262/#sec-array.from
    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/from
    pub(crate) fn from(
        this: &JsValue,
        args: &[JsValue],
        context: &mut Context,
    ) -> JsResult<JsValue> {
        let items = args.get_or_undefined(0);
        let mapfn = args.get_or_undefined(1);
        let this_arg = args.get_or_undefined(2);

        // 2. If mapfn is undefined, let mapping be false
        // 3. Else,
        //     a. If IsCallable(mapfn) is false, throw a TypeError exception.
        //     b. Let mapping be true.
        let mapping = match mapfn {
            JsValue::Undefined => None,
            JsValue::Object(o) if o.is_callable() => Some(o),
            _ => return context.throw_type_error(format!("{} is not a function", mapfn.type_of())),
        };

        // 4. Let usingIterator be ? GetMethod(items, @@iterator).
        let using_iterator = items
            .get_method(WellKnownSymbols::iterator(), context)?
            .map(JsValue::from);

        if let Some(using_iterator) = using_iterator {
            // 5. If usingIterator is not undefined, then

            // a. If IsConstructor(C) is true, then
            //     i. Let A be ? Construct(C).
            // b. Else,
            //     i. Let A be ? ArrayCreate(0en).
            let a = match this.as_constructor() {
                Some(constructor) => constructor.construct(&[], None, context)?,
                _ => Self::array_create(0, None, context)?,
            };

            // c. Let iteratorRecord be ? GetIterator(items, sync, usingIterator).
            let iterator_record =
                items.get_iterator(context, Some(IteratorHint::Sync), Some(using_iterator))?;

            // d. Let k be 0.
            // e. Repeat,
            //     i. If k ≥ 2^53 - 1 (MAX_SAFE_INTEGER), then
            //     ...
            //     x. Set k to k + 1.
            for k in 0..9_007_199_254_740_991_u64 {
                // iii. Let next be ? IteratorStep(iteratorRecord).
                let next = iterator_record.step(context)?;

                // iv. If next is false, then
                let next = if let Some(next) = next {
                    next
                } else {
                    // 1. Perform ? Set(A, "length", 𝔽(k), true).
                    a.set("length", k, true, context)?;

                    // 2. Return A.
                    return Ok(a.into());
                };

                // v. Let nextValue be ? IteratorValue(next).
                let next_value = next.value(context)?;

                // vi. If mapping is true, then
                let mapped_value = if let Some(mapfn) = mapping {
                    // 1. Let mappedValue be Call(mapfn, thisArg, « nextValue, 𝔽(k) »).
                    let mapped_value = mapfn.call(this_arg, &[next_value, k.into()], context);

                    // 2. IfAbruptCloseIterator(mappedValue, iteratorRecord).
                    if_abrupt_close_iterator!(mapped_value, iterator_record, context)
                } else {
                    // vii. Else, let mappedValue be nextValue.
                    next_value
                };

                // viii. Let defineStatus be CreateDataPropertyOrThrow(A, Pk, mappedValue).
                let define_status = a.create_data_property_or_throw(k, mapped_value, context);

                // ix. IfAbruptCloseIterator(defineStatus, iteratorRecord).
                if_abrupt_close_iterator!(define_status, iterator_record, context);
            }

            // NOTE: The loop above has to return before it reaches iteration limit,
            // which is why it's safe to have this as the fallback return
            //
            // 1. Let error be ThrowCompletion(a newly created TypeError object).
            let error = context.throw_type_error("Invalid array length");

            // 2. Return ? IteratorClose(iteratorRecord, error).
            iterator_record.close(error, context)
        } else {
            // 6. NOTE: items is not an Iterable so assume it is an array-like object.
            // 7. Let arrayLike be ! ToObject(items).
            let array_like = items
                .to_object(context)
                .expect("should not fail according to spec");

            // 8. Let len be ? LengthOfArrayLike(arrayLike).
            let len = array_like.length_of_array_like(context)?;

            // 9. If IsConstructor(C) is true, then
            //     a. Let A be ? Construct(C, « 𝔽(len) »).
            // 10. Else,
            //     a. Let A be ? ArrayCreate(len).
            let a = match this.as_constructor() {
                Some(constructor) => constructor.construct(&[len.into()], None, context)?,
                _ => Self::array_create(len, None, context)?,
            };

            // 11. Let k be 0.
            // 12. Repeat, while k < len,
            //     ...
            //     f. Set k to k + 1.
            for k in 0..len {
                // a. Let Pk be ! ToString(𝔽(k)).
                // b. Let kValue be ? Get(arrayLike, Pk).
                let k_value = array_like.get(k, context)?;

                let mapped_value = if let Some(mapfn) = mapping {
                    // c. If mapping is true, then
                    //     i. Let mappedValue be ? Call(mapfn, thisArg, « kValue, 𝔽(k) »).
                    mapfn.call(this_arg, &[k_value, k.into()], context)?
                } else {
                    // d. Else, let mappedValue be kValue.
                    k_value
                };

                // e. Perform ? CreateDataPropertyOrThrow(A, Pk, mappedValue).
                a.create_data_property_or_throw(k, mapped_value, context)?;
            }

            // 13. Perform ? Set(A, "length", 𝔽(len), true).
            a.set("length", len, true, context)?;

            // 14. Return A.
            Ok(a.into())
        }
    }

    /// `Array.isArray( arg )`
    ///
    /// The isArray function takes one argument arg, and returns the Boolean value true
    /// if the argument is an object whose class internal property is "Array"; otherwise it returns false.
    ///
    /// More information:
    ///  - [ECMAScript reference][spec]
    ///  - [MDN documentation][mdn]
    ///
    /// [spec]: https://tc39.es/ecma262/#sec-array.isarray
    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/isArray
    pub(crate) fn is_array(
        _: &JsValue,
        args: &[JsValue],
        context: &mut Context,
    ) -> JsResult<JsValue> {
        // 1. Return ? IsArray(arg).
        args.get_or_undefined(0).is_array(context).map(Into::into)
    }

    /// `Array.of(...items)`
    ///
    /// The Array.of method creates a new Array instance from a variable number of arguments,
    /// regardless of the number or type of arguments.
    ///
    /// More information:
    ///  - [ECMAScript reference][spec]
    ///  - [MDN documentation][mdn]
    ///
    /// [spec]: https://tc39.es/ecma262/#sec-array.of
    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/of
    pub(crate) fn of(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
        // 1. Let len be the number of elements in items.
        // 2. Let lenNumber be 𝔽(len).
        let len = args.len();

        // 3. Let C be the this value.
        // 4. If IsConstructor(C) is true, then
        //     a. Let A be ? Construct(C, « lenNumber »).
        // 5. Else,
        //     a. Let A be ? ArrayCreate(len).
        let a = match this.as_constructor() {
            Some(constructor) => constructor.construct(&[len.into()], None, context)?,
            _ => Self::array_create(len, None, context)?,
        };

        // 6. Let k be 0.
        // 7. Repeat, while k < len,
        for (k, value) in args.iter().enumerate() {
            // a. Let kValue be items[k].
            // b. Let Pk be ! ToString(𝔽(k)).
            // c. Perform ? CreateDataPropertyOrThrow(A, Pk, kValue).
            a.create_data_property_or_throw(k, value, context)?;
            // d. Set k to k + 1.
        }

        // 8. Perform ? Set(A, "length", lenNumber, true).
        a.set("length", len, true, context)?;

        // 9. Return A.
        Ok(a.into())
    }

    ///'Array.prototype.at(index)'
    ///
    /// The at() method takes an integer value and returns the item at that
    /// index, allowing for positive and negative integers. Negative integers
    /// count back from the last item in the array.
    ///
    /// More information:
    ///  - [ECMAScript reference][spec]
    ///  - [MDN documentation][mdn]
    ///
    /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.at
    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/at
    pub(crate) fn at(this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
        //1. let O be ? ToObject(this value)
        let obj = this.to_object(context)?;
        //2. let len be ? LengthOfArrayLike(O)
        let len = obj.length_of_array_like(context)? as i64;
        //3. let relativeIndex be ? ToIntegerOrInfinity(index)
        let relative_index = args.get_or_undefined(0).to_integer_or_infinity(context)?;
        let k = match relative_index {
            //4. if relativeIndex >= 0, then let k be relativeIndex
            //check if positive and if below length of array
            IntegerOrInfinity::Integer(i) if i >= 0 && i < len => i,
            //5. Else, let k be len + relativeIndex
            //integer should be negative, so abs() and check if less than or equal to length of array
            IntegerOrInfinity::Integer(i) if i < 0 && i.abs() <= len => len + i,
            //handle most likely impossible case of
            //IntegerOrInfinity::NegativeInfinity || IntegerOrInfinity::PositiveInfinity
            //by returning undefined
            _ => return Ok(JsValue::undefined()),
        };
        //6. if k < 0  or k >= len,
        //handled by the above match guards
        //7. Return ? Get(O, !ToString(𝔽(k)))
        obj.get(k, context)
    }

    /// `Array.prototype.concat(...arguments)`
    ///
    /// When the concat method is called with zero or more arguments, it returns an
    /// array containing the array elements of the object followed by the array
    /// elements of each argument in order.
    ///
    /// More information:
    ///  - [ECMAScript reference][spec]
    ///  - [MDN documentation][mdn]
    ///
    /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.concat
    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/concat
    pub(crate) fn concat(
        this: &JsValue,
        args: &[JsValue],
        context: &mut Context,
    ) -> JsResult<JsValue> {
        // 1. Let O be ? ToObject(this value).
        let obj = this.to_object(context)?;
        // 2. Let A be ? ArraySpeciesCreate(O, 0).
        let arr = Self::array_species_create(&obj, 0, context)?;
        // 3. Let n be 0.
        let mut n = 0;
        // 4. Prepend O to items.
        // 5. For each element E of items, do
        for item in [JsValue::new(obj)].iter().chain(args.iter()) {
            // a. Let spreadable be ? IsConcatSpreadable(E).
            let spreadable = Self::is_concat_spreadable(item, context)?;
            // b. If spreadable is true, then
            if spreadable {
                // item is guaranteed to be an object since is_concat_spreadable checks it,
                // so we can call `.unwrap()`
                let item = item.as_object().expect("guaranteed to be an object");
                // i. Let k be 0.
                // ii. Let len be ? LengthOfArrayLike(E).
                let len = item.length_of_array_like(context)?;
                // iii. If n + len > 2^53 - 1, throw a TypeError exception.
                if n + len > Number::MAX_SAFE_INTEGER as usize {
                    return context.throw_type_error(
                        "length + number of arguments exceeds the max safe integer limit",
                    );
                }
                // iv. Repeat, while k < len,
                for k in 0..len {
                    // 1. Let P be ! ToString(𝔽(k)).
                    // 2. Let exists be ? HasProperty(E, P).
                    let exists = item.has_property(k, context)?;
                    // 3. If exists is true, then
                    if exists {
                        // a. Let subElement be ? Get(E, P).
                        let sub_element = item.get(k, context)?;
                        // b. Perform ? CreateDataPropertyOrThrow(A, ! ToString(𝔽(n)), subElement).
                        arr.create_data_property_or_throw(n, sub_element, context)?;
                    }
                    // 4. Set n to n + 1.
                    n += 1;
                    // 5. Set k to k + 1.
                }
            }
            // c. Else,
            else {
                // i. NOTE: E is added as a single item rather than spread.
                // ii. If n ≥ 2^53 - 1, throw a TypeError exception.
                if n >= Number::MAX_SAFE_INTEGER as usize {
                    return context.throw_type_error("length exceeds the max safe integer limit");
                }
                // iii. Perform ? CreateDataPropertyOrThrow(A, ! ToString(𝔽(n)), E).
                arr.create_data_property_or_throw(n, item, context)?;
                // iv. Set n to n + 1.
                n += 1;
            }
        }
        // 6. Perform ? Set(A, "length", 𝔽(n), true).
        arr.set("length", n, true, context)?;

        // 7. Return A.
        Ok(JsValue::new(arr))
    }

    /// `Array.prototype.push( ...items )`
    ///
    /// The arguments are appended to the end of the array, in the order in which
    /// they appear. The new length of the array is returned as the result of the
    /// call.
    ///
    /// More information:
    ///  - [ECMAScript reference][spec]
    ///  - [MDN documentation][mdn]
    ///
    /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.push
    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/push
    pub(crate) fn push(
        this: &JsValue,
        args: &[JsValue],
        context: &mut Context,
    ) -> JsResult<JsValue> {
        // 1. Let O be ? ToObject(this value).
        let o = this.to_object(context)?;
        // 2. Let len be ? LengthOfArrayLike(O).
        let mut len = o.length_of_array_like(context)? as u64;
        // 3. Let argCount be the number of elements in items.
        let arg_count = args.len() as u64;
        // 4. If len + argCount > 2^53 - 1, throw a TypeError exception.
        if len + arg_count > 2u64.pow(53) - 1 {
            return context.throw_type_error(
                "the length + the number of arguments exceed the maximum safe integer limit",
            );
        }
        // 5. For each element E of items, do
        for element in args.iter().cloned() {
            // a. Perform ? Set(O, ! ToString(𝔽(len)), E, true).
            o.set(len, element, true, context)?;
            // b. Set len to len + 1.
            len += 1;
        }
        // 6. Perform ? Set(O, "length", 𝔽(len), true).
        o.set("length", len, true, context)?;
        // 7. Return 𝔽(len).
        Ok(len.into())
    }

    /// `Array.prototype.pop()`
    ///
    /// The last element of the array is removed from the array and returned.
    ///
    /// More information:
    ///  - [ECMAScript reference][spec]
    ///  - [MDN documentation][mdn]
    ///
    /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.pop
    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/pop
    pub(crate) fn pop(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
        // 1. Let O be ? ToObject(this value).
        let o = this.to_object(context)?;
        // 2. Let len be ? LengthOfArrayLike(O).
        let len = o.length_of_array_like(context)?;
        // 3. If len = 0, then
        if len == 0 {
            // a. Perform ? Set(O, "length", +0𝔽, true).
            o.set("length", 0, true, context)?;
            // b. Return undefined.
            Ok(JsValue::undefined())
        // 4. Else,
        } else {
            // a. Assert: len > 0.
            // b. Let newLen be 𝔽(len - 1).
            let new_len = len - 1;
            // c. Let index be ! ToString(newLen).
            let index = new_len;
            // d. Let element be ? Get(O, index).
            let element = o.get(index, context)?;
            // e. Perform ? DeletePropertyOrThrow(O, index).
            o.delete_property_or_throw(index, context)?;
            // f. Perform ? Set(O, "length", newLen, true).
            o.set("length", new_len, true, context)?;
            // g. Return element.
            Ok(element)
        }
    }

    /// `Array.prototype.forEach( callbackFn [ , thisArg ] )`
    ///
    /// This method executes the provided callback function for each element in the array.
    ///
    /// More information:
    ///  - [ECMAScript reference][spec]
    ///  - [MDN documentation][mdn]
    ///
    /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.foreach
    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/forEach
    pub(crate) fn for_each(
        this: &JsValue,
        args: &[JsValue],
        context: &mut Context,
    ) -> JsResult<JsValue> {
        // 1. Let O be ? ToObject(this value).
        let o = this.to_object(context)?;
        // 2. Let len be ? LengthOfArrayLike(O).
        let len = o.length_of_array_like(context)?;
        // 3. If IsCallable(callbackfn) is false, throw a TypeError exception.
        let callback = args.get_or_undefined(0).as_callable().ok_or_else(|| {
            context.construct_type_error("Array.prototype.forEach: invalid callback function")
        })?;
        // 4. Let k be 0.
        // 5. Repeat, while k < len,
        for k in 0..len {
            // a. Let Pk be ! ToString(𝔽(k)).
            let pk = k;
            // b. Let kPresent be ? HasProperty(O, Pk).
            let present = o.has_property(pk, context)?;
            // c. If kPresent is true, then
            if present {
                // i. Let kValue be ? Get(O, Pk).
                let k_value = o.get(pk, context)?;
                // ii. Perform ? Call(callbackfn, thisArg, « kValue, 𝔽(k), O »).
                let this_arg = args.get_or_undefined(1);
                callback.call(this_arg, &[k_value, k.into(), o.clone().into()], context)?;
            }
            // d. Set k to k + 1.
        }
        // 6. Return undefined.
        Ok(JsValue::undefined())
    }

    /// `Array.prototype.join( separator )`
    ///
    /// The elements of the array are converted to Strings, and these Strings are
    /// then concatenated, separated by occurrences of the separator. If no
    /// separator is provided, a single comma is used as the separator.
    ///
    /// More information:
    ///  - [ECMAScript reference][spec]
    ///  - [MDN documentation][mdn]
    ///
    /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.join
    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/join
    pub(crate) fn join(
        this: &JsValue,
        args: &[JsValue],
        context: &mut Context,
    ) -> JsResult<JsValue> {
        // 1. Let O be ? ToObject(this value).
        let o = this.to_object(context)?;
        // 2. Let len be ? LengthOfArrayLike(O).
        let len = o.length_of_array_like(context)?;
        // 3. If separator is undefined, let sep be the single-element String ",".
        // 4. Else, let sep be ? ToString(separator).
        let separator = args.get_or_undefined(0);
        let separator = if separator.is_undefined() {
            JsString::new(",")
        } else {
            separator.to_string(context)?
        };

        // 5. Let R be the empty String.
        let mut r = String::new();
        // 6. Let k be 0.
        // 7. Repeat, while k < len,
        for k in 0..len {
            // a. If k > 0, set R to the string-concatenation of R and sep.
            if k > 0 {
                r.push_str(&separator);
            }
            // b. Let element be ? Get(O, ! ToString(𝔽(k))).
            let element = o.get(k, context)?;
            // c. If element is undefined or null, let next be the empty String; otherwise, let next be ? ToString(element).
            let next = if element.is_null_or_undefined() {
                JsString::new("")
            } else {
                element.to_string(context)?
            };
            // d. Set R to the string-concatenation of R and next.
            r.push_str(&next);
            // e. Set k to k + 1.
        }
        // 8. Return R.
        Ok(r.into())
    }

    /// `Array.prototype.toString( separator )`
    ///
    /// The toString function is intentionally generic; it does not require that
    /// its this value be an Array object. Therefore it can be transferred to
    /// other kinds of objects for use as a method.
    ///
    /// More information:
    ///  - [ECMAScript reference][spec]
    ///  - [MDN documentation][mdn]
    ///
    /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.tostring
    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/toString
    #[allow(clippy::wrong_self_convention)]
    pub(crate) fn to_string(
        this: &JsValue,
        _: &[JsValue],
        context: &mut Context,
    ) -> JsResult<JsValue> {
        // 1. Let array be ? ToObject(this value).
        let array = this.to_object(context)?;
        // 2. Let func be ? Get(array, "join").
        let func = array.get("join", context)?;
        // 3. If IsCallable(func) is false, set func to the intrinsic function %Object.prototype.toString%.
        // 4. Return ? Call(func, array).
        if let Some(func) = func.as_callable() {
            func.call(&array.into(), &[], context)
        } else {
            crate::builtins::object::Object::to_string(&array.into(), &[], context)
        }
    }

    /// `Array.prototype.reverse()`
    ///
    /// The elements of the array are rearranged so as to reverse their order.
    /// The object is returned as the result of the call.
    ///
    /// More information:
    ///  - [ECMAScript reference][spec]
    ///  - [MDN documentation][mdn]
    ///
    /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.reverse
    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reverse
    #[allow(clippy::else_if_without_else)]
    pub(crate) fn reverse(
        this: &JsValue,
        _: &[JsValue],
        context: &mut Context,
    ) -> JsResult<JsValue> {
        // 1. Let O be ? ToObject(this value).
        let o = this.to_object(context)?;
        // 2. Let len be ? LengthOfArrayLike(O).
        let len = o.length_of_array_like(context)?;
        // 3. Let middle be floor(len / 2).
        let middle = len / 2;
        // 4. Let lower be 0.
        let mut lower = 0;
        // 5. Repeat, while lower ≠ middle,
        while lower != middle {
            // a. Let upper be len - lower - 1.
            let upper = len - lower - 1;
            // Skipped: b. Let upperP be ! ToString(𝔽(upper)).
            // Skipped: c. Let lowerP be ! ToString(𝔽(lower)).
            // d. Let lowerExists be ? HasProperty(O, lowerP).
            let lower_exists = o.has_property(lower, context)?;
            // e. If lowerExists is true, then
            let mut lower_value = JsValue::undefined();
            if lower_exists {
                // i. Let lowerValue be ? Get(O, lowerP).
                lower_value = o.get(lower, context)?;
            }
            // f. Let upperExists be ? HasProperty(O, upperP).
            let upper_exists = o.has_property(upper, context)?;
            // g. If upperExists is true, then
            let mut upper_value = JsValue::undefined();
            if upper_exists {
                // i. Let upperValue be ? Get(O, upperP).
                upper_value = o.get(upper, context)?;
            }
            match (lower_exists, upper_exists) {
                // h. If lowerExists is true and upperExists is true, then
                (true, true) => {
                    // i. Perform ? Set(O, lowerP, upperValue, true).
                    o.set(lower, upper_value, true, context)?;
                    // ii. Perform ? Set(O, upperP, lowerValue, true).
                    o.set(upper, lower_value, true, context)?;
                }
                // i. Else if lowerExists is false and upperExists is true, then
                (false, true) => {
                    // i. Perform ? Set(O, lowerP, upperValue, true).
                    o.set(lower, upper_value, true, context)?;
                    // ii. Perform ? DeletePropertyOrThrow(O, upperP).
                    o.delete_property_or_throw(upper, context)?;
                }
                // j. Else if lowerExists is true and upperExists is false, then
                (true, false) => {
                    // i. Perform ? DeletePropertyOrThrow(O, lowerP).
                    o.delete_property_or_throw(lower, context)?;
                    // ii. Perform ? Set(O, upperP, lowerValue, true).
                    o.set(upper, lower_value, true, context)?;
                }
                // k. Else,
                (false, false) => {
                    // i. Assert: lowerExists and upperExists are both false.
                    // ii. No action is required.
                }
            }

            // l. Set lower to lower + 1.
            lower += 1;
        }
        // 6. Return O.
        Ok(o.into())
    }

    /// `Array.prototype.shift()`
    ///
    /// The first element of the array is removed from the array and returned.
    ///
    /// More information:
    ///  - [ECMAScript reference][spec]
    ///  - [MDN documentation][mdn]
    ///
    /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.shift
    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/shift
    pub(crate) fn shift(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
        // 1. Let O be ? ToObject(this value).
        let o = this.to_object(context)?;
        // 2. Let len be ? LengthOfArrayLike(O).
        let len = o.length_of_array_like(context)?;
        // 3. If len = 0, then
        if len == 0 {
            // a. Perform ? Set(O, "length", +0𝔽, true).
            o.set("length", 0, true, context)?;
            // b. Return undefined.
            return Ok(JsValue::undefined());
        }
        // 4. Let first be ? Get(O, "0").
        let first = o.get(0, context)?;
        // 5. Let k be 1.
        // 6. Repeat, while k < len,
        for k in 1..len {
            // a. Let from be ! ToString(𝔽(k)).
            let from = k;
            // b. Let to be ! ToString(𝔽(k - 1)).
            let to = k - 1;
            // c. Let fromPresent be ? HasProperty(O, from).
            let from_present = o.has_property(from, context)?;
            // d. If fromPresent is true, then
            if from_present {
                // i. Let fromVal be ? Get(O, from).
                let from_val = o.get(from, context)?;
                // ii. Perform ? Set(O, to, fromVal, true).
                o.set(to, from_val, true, context)?;
            // e. Else,
            } else {
                // i. Assert: fromPresent is false.
                // ii. Perform ? DeletePropertyOrThrow(O, to).
                o.delete_property_or_throw(to, context)?;
            }
            // f. Set k to k + 1.
        }
        // 7. Perform ? DeletePropertyOrThrow(O, ! ToString(𝔽(len - 1))).
        o.delete_property_or_throw(len - 1, context)?;
        // 8. Perform ? Set(O, "length", 𝔽(len - 1), true).
        o.set("length", len - 1, true, context)?;
        // 9. Return first.
        Ok(first)
    }

    /// `Array.prototype.unshift( ...items )`
    ///
    /// The arguments are prepended to the start of the array, such that their order
    /// within the array is the same as the order in which they appear in the
    /// argument list.
    ///
    /// More information:
    ///  - [ECMAScript reference][spec]
    ///  - [MDN documentation][mdn]
    ///
    /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.unshift
    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/unshift
    pub(crate) fn unshift(
        this: &JsValue,
        args: &[JsValue],
        context: &mut Context,
    ) -> JsResult<JsValue> {
        // 1. Let O be ? ToObject(this value).
        let o = this.to_object(context)?;
        // 2. Let len be ? LengthOfArrayLike(O).
        let len = o.length_of_array_like(context)? as u64;
        // 3. Let argCount be the number of elements in items.
        let arg_count = args.len() as u64;
        // 4. If argCount > 0, then
        if arg_count > 0 {
            // a. If len + argCount > 2^53 - 1, throw a TypeError exception.
            if len + arg_count > 2u64.pow(53) - 1 {
                return context.throw_type_error(
                    "length + number of arguments exceeds the max safe integer limit",
                );
            }
            // b. Let k be len.
            let mut k = len;
            // c. Repeat, while k > 0,
            while k > 0 {
                // i. Let from be ! ToString(𝔽(k - 1)).
                let from = k - 1;
                // ii. Let to be ! ToString(𝔽(k + argCount - 1)).
                let to = k + arg_count - 1;
                // iii. Let fromPresent be ? HasProperty(O, from).
                let from_present = o.has_property(from, context)?;
                // iv. If fromPresent is true, then
                if from_present {
                    // 1. Let fromValue be ? Get(O, from).
                    let from_value = o.get(from, context)?;
                    // 2. Perform ? Set(O, to, fromValue, true).
                    o.set(to, from_value, true, context)?;
                // v. Else,
                } else {
                    // 1. Assert: fromPresent is false.
                    // 2. Perform ? DeletePropertyOrThrow(O, to).
                    o.delete_property_or_throw(to, context)?;
                }
                // vi. Set k to k - 1.
                k -= 1;
            }
            // d. Let j be +0𝔽.
            // e. For each element E of items, do
            for (j, e) in args.iter().enumerate() {
                // i. Perform ? Set(O, ! ToString(j), E, true).
                o.set(j, e, true, context)?;
                // ii. Set j to j + 1𝔽.
            }
        }
        // 5. Perform ? Set(O, "length", 𝔽(len + argCount), true).
        o.set("length", len + arg_count, true, context)?;
        // 6. Return 𝔽(len + argCount).
        Ok((len + arg_count).into())
    }

    /// `Array.prototype.every( callback, [ thisArg ] )`
    ///
    /// The every method executes the provided callback function once for each
    /// element present in the array until it finds the one where callback returns
    /// a falsy value. It returns `false` if it finds such element, otherwise it
    /// returns `true`.
    ///
    /// More information:
    ///  - [ECMAScript reference][spec]
    ///  - [MDN documentation][mdn]
    ///
    /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.every
    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/every
    pub(crate) fn every(
        this: &JsValue,
        args: &[JsValue],
        context: &mut Context,
    ) -> JsResult<JsValue> {
        // 1. Let O be ? ToObject(this value).
        let o = this.to_object(context)?;
        // 2. Let len be ? LengthOfArrayLike(O).
        let len = o.length_of_array_like(context)?;
        // 3. If IsCallable(callbackfn) is false, throw a TypeError exception.
        let callback = args.get_or_undefined(0).as_callable().ok_or_else(|| {
            context.construct_type_error("Array.prototype.every: callback is not callable")
        })?;

        let this_arg = args.get_or_undefined(1);

        // 4. Let k be 0.
        // 5. Repeat, while k < len,
        for k in 0..len {
            // a. Let Pk be ! ToString(𝔽(k)).
            // b. Let kPresent be ? HasProperty(O, Pk).
            let k_present = o.has_property(k, context)?;
            // c. If kPresent is true, then
            if k_present {
                // i. Let kValue be ? Get(O, Pk).
                let k_value = o.get(k, context)?;
                // ii. Let testResult be ! ToBoolean(? Call(callbackfn, thisArg, « kValue, 𝔽(k), O »)).
                let test_result = callback
                    .call(this_arg, &[k_value, k.into(), o.clone().into()], context)?
                    .to_boolean();
                // iii. If testResult is false, return false.
                if !test_result {
                    return Ok(JsValue::new(false));
                }
            }
            // d. Set k to k + 1.
        }
        // 6. Return true.
        Ok(JsValue::new(true))
    }

    /// `Array.prototype.map( callback, [ thisArg ] )`
    ///
    /// For each element in the array the callback function is called, and a new
    /// array is constructed from the return values of these calls.
    ///
    /// More information:
    ///  - [ECMAScript reference][spec]
    ///  - [MDN documentation][mdn]
    ///
    /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.map
    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/map
    pub(crate) fn map(
        this: &JsValue,
        args: &[JsValue],
        context: &mut Context,
    ) -> JsResult<JsValue> {
        // 1. Let O be ? ToObject(this value).
        let o = this.to_object(context)?;
        // 2. Let len be ? LengthOfArrayLike(O).
        let len = o.length_of_array_like(context)?;
        // 3. If IsCallable(callbackfn) is false, throw a TypeError exception.
        let callback = args.get_or_undefined(0).as_callable().ok_or_else(|| {
            context.construct_type_error("Array.prototype.map: Callbackfn is not callable")
        })?;

        // 4. Let A be ? ArraySpeciesCreate(O, len).
        let a = Self::array_species_create(&o, len, context)?;

        let this_arg = args.get_or_undefined(1);

        // 5. Let k be 0.
        // 6. Repeat, while k < len,
        for k in 0..len {
            // a. Let Pk be ! ToString(𝔽(k)).
            // b. Let k_present be ? HasProperty(O, Pk).
            let k_present = o.has_property(k, context)?;
            // c. If k_present is true, then
            if k_present {
                // i. Let kValue be ? Get(O, Pk).
                let k_value = o.get(k, context)?;
                // ii. Let mappedValue be ? Call(callbackfn, thisArg, « kValue, 𝔽(k), O »).
                let mapped_value =
                    callback.call(this_arg, &[k_value, k.into(), this.into()], context)?;
                // iii. Perform ? CreateDataPropertyOrThrow(A, Pk, mappedValue).
                a.create_data_property_or_throw(k, mapped_value, context)?;
            }
            // d. Set k to k + 1.
        }
        // 7. Return A.
        Ok(a.into())
    }

    /// `Array.prototype.indexOf( searchElement[, fromIndex ] )`
    ///
    /// More information:
    ///  - [ECMAScript reference][spec]
    ///  - [MDN documentation][mdn]
    ///
    /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.indexof
    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/indexOf
    pub(crate) fn index_of(
        this: &JsValue,
        args: &[JsValue],
        context: &mut Context,
    ) -> JsResult<JsValue> {
        // 1. Let O be ? ToObject(this value).
        let o = this.to_object(context)?;

        // 2. Let len be ? LengthOfArrayLike(O).
        let len = o.length_of_array_like(context)? as i64;

        // 3. If len is 0, return -1𝔽.
        if len == 0 {
            return Ok(JsValue::new(-1));
        }

        // 4. Let n be ? ToIntegerOrInfinity(fromIndex).
        let n = args
            .get(1)
            .cloned()
            .unwrap_or_default()
            .to_integer_or_infinity(context)?;
        // 5. Assert: If fromIndex is undefined, then n is 0.
        let n = match n {
            // 6. If n is +∞, return -1𝔽.
            IntegerOrInfinity::PositiveInfinity => return Ok(JsValue::new(-1)),
            // 7. Else if n is -∞, set n to 0.
            IntegerOrInfinity::NegativeInfinity => 0,
            IntegerOrInfinity::Integer(value) => value,
        };

        // 8. If n ≥ 0, then
        let mut k;
        if n >= 0 {
            // a. Let k be n.
            k = n;
        // 9. Else,
        } else {
            // a. Let k be len + n.
            k = len + n;
            // b. If k < 0, set k to 0.
            if k < 0 {
                k = 0;
            }
        };

        let search_element = args.get_or_undefined(0);

        // 10. Repeat, while k < len,
        while k < len {
            // a. Let kPresent be ? HasProperty(O, ! ToString(𝔽(k))).
            let k_present = o.has_property(k, context)?;
            // b. If kPresent is true, then
            if k_present {
                // i. Let elementK be ? Get(O, ! ToString(𝔽(k))).
                let element_k = o.get(k, context)?;
                // ii. Let same be IsStrictlyEqual(searchElement, elementK).
                // iii. If same is true, return 𝔽(k).
                if search_element.strict_equals(&element_k) {
                    return Ok(JsValue::new(k));
                }
            }
            // c. Set k to k + 1.
            k += 1;
        }
        // 11. Return -1𝔽.
        Ok(JsValue::new(-1))
    }

    /// `Array.prototype.lastIndexOf( searchElement[, fromIndex ] )`
    ///
    ///
    /// `lastIndexOf` compares searchElement to the elements of the array in descending order
    /// using the Strict Equality Comparison algorithm, and if found at one or more indices,
    /// returns the largest such index; otherwise, -1 is returned.
    ///
    /// The optional second argument fromIndex defaults to the array's length minus one
    /// (i.e. the whole array is searched). If it is greater than or equal to the length of the array,
    /// the whole array will be searched. If it is negative, it is used as the offset from the end
    /// of the array to compute fromIndex. If the computed index is less than 0, -1 is returned.
    ///
    /// More information:
    ///  - [ECMAScript reference][spec]
    ///  - [MDN documentation][mdn]
    ///
    /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.lastindexof
    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/lastIndexOf
    pub(crate) fn last_index_of(
        this: &JsValue,
        args: &[JsValue],
        context: &mut Context,
    ) -> JsResult<JsValue> {
        // 1. Let O be ? ToObject(this value).
        let o = this.to_object(context)?;

        // 2. Let len be ? LengthOfArrayLike(O).
        let len = o.length_of_array_like(context)? as i64;

        // 3. If len is 0, return -1𝔽.
        if len == 0 {
            return Ok(JsValue::new(-1));
        }

        // 4. If fromIndex is present, let n be ? ToIntegerOrInfinity(fromIndex); else let n be len - 1.
        let n = if let Some(from_index) = args.get(1) {
            from_index.to_integer_or_infinity(context)?
        } else {
            IntegerOrInfinity::Integer(len - 1)
        };

        let mut k = match n {
            // 5. If n is -∞, return -1𝔽.
            IntegerOrInfinity::NegativeInfinity => return Ok(JsValue::new(-1)),
            // 6. If n ≥ 0, then
            //     a. Let k be min(n, len - 1).
            IntegerOrInfinity::Integer(n) if n >= 0 => min(n, len - 1),
            IntegerOrInfinity::PositiveInfinity => len - 1,
            // 7. Else,
            //     a. Let k be len + n.
            IntegerOrInfinity::Integer(n) => len + n,
        };

        let search_element = args.get_or_undefined(0);

        // 8. Repeat, while k ≥ 0,
        while k >= 0 {
            // a. Let kPresent be ? HasProperty(O, ! ToString(𝔽(k))).
            let k_present = o.has_property(k, context)?;
            // b. If kPresent is true, then
            if k_present {
                // i. Let elementK be ? Get(O, ! ToString(𝔽(k))).
                let element_k = o.get(k, context)?;
                // ii. Let same be IsStrictlyEqual(searchElement, elementK).
                // iii. If same is true, return 𝔽(k).
                if JsValue::strict_equals(search_element, &element_k) {
                    return Ok(JsValue::new(k));
                }
            }
            // c. Set k to k - 1.
            k -= 1;
        }
        // 9. Return -1𝔽.
        Ok(JsValue::new(-1))
    }

    /// `Array.prototype.find( callback, [thisArg] )`
    ///
    /// The find method executes the callback function once for each index of the array
    /// until the callback returns a truthy value. If so, find immediately returns the value
    /// of that element. Otherwise, find returns undefined.
    ///
    /// More information:
    ///  - [ECMAScript reference][spec]
    ///  - [MDN documentation][mdn]
    ///
    /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.find
    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/find
    pub(crate) fn find(
        this: &JsValue,
        args: &[JsValue],
        context: &mut Context,
    ) -> JsResult<JsValue> {
        // 1. Let O be ? ToObject(this value).
        let o = this.to_object(context)?;

        // 2. Let len be ? LengthOfArrayLike(O).
        let len = o.length_of_array_like(context)?;

        // 3. If IsCallable(predicate) is false, throw a TypeError exception.
        let predicate = args.get_or_undefined(0).as_callable().ok_or_else(|| {
            context.construct_type_error("Array.prototype.find: predicate is not callable")
        })?;

        let this_arg = args.get_or_undefined(1);

        // 4. Let k be 0.
        let mut k = 0;
        // 5. Repeat, while k < len,
        while k < len {
            // a. Let Pk be ! ToString(𝔽(k)).
            let pk = k;
            // b. Let kValue be ? Get(O, Pk).
            let k_value = o.get(pk, context)?;
            // c. Let testResult be ! ToBoolean(? Call(predicate, thisArg, « kValue, 𝔽(k), O »)).
            let test_result = predicate
                .call(
                    this_arg,
                    &[k_value.clone(), k.into(), o.clone().into()],
                    context,
                )?
                .to_boolean();
            // d. If testResult is true, return kValue.
            if test_result {
                return Ok(k_value);
            }
            // e. Set k to k + 1.
            k += 1;
        }
        // 6. Return undefined.
        Ok(JsValue::undefined())
    }

    /// `Array.prototype.findIndex( predicate [ , thisArg ] )`
    ///
    /// This method executes the provided predicate function for each element of the array.
    /// If the predicate function returns `true` for an element, this method returns the index of the element.
    /// If all elements return `false`, the value `-1` is returned.
    ///
    /// More information:
    ///  - [ECMAScript reference][spec]
    ///  - [MDN documentation][mdn]
    ///
    /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.findindex
    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/findIndex
    pub(crate) fn find_index(
        this: &JsValue,
        args: &[JsValue],
        context: &mut Context,
    ) -> JsResult<JsValue> {
        // 1. Let O be ? ToObject(this value).
        let o = this.to_object(context)?;

        // 2. Let len be ? LengthOfArrayLike(O).
        let len = o.length_of_array_like(context)?;

        // 3. If IsCallable(predicate) is false, throw a TypeError exception.
        let predicate = args.get_or_undefined(0).as_callable().ok_or_else(|| {
            context.construct_type_error("Array.prototype.findIndex: predicate is not callable")
        })?;

        let this_arg = args.get_or_undefined(1);

        // 4. Let k be 0.
        let mut k = 0;
        // 5. Repeat, while k < len,
        while k < len {
            // a. Let Pk be ! ToString(𝔽(k)).
            let pk = k;
            // b. Let kValue be ? Get(O, Pk).
            let k_value = o.get(pk, context)?;
            // c. Let testResult be ! ToBoolean(? Call(predicate, thisArg, « kValue, 𝔽(k), O »)).
            let test_result = predicate
                .call(this_arg, &[k_value, k.into(), o.clone().into()], context)?
                .to_boolean();
            // d. If testResult is true, return 𝔽(k).
            if test_result {
                return Ok(JsValue::new(k));
            }
            // e. Set k to k + 1.
            k += 1;
        }
        // 6. Return -1𝔽.
        Ok(JsValue::new(-1))
    }

    /// `Array.prototype.findLast( predicate, [thisArg] )`
    ///
    /// findLast calls predicate once for each element of the array, in descending order,
    /// until it finds one where predicate returns true. If such an element is found, findLast
    /// immediately returns that element value. Otherwise, findLast returns undefined.
    ///
    /// More information:
    ///  - [ECMAScript proposal][spec]
    ///
    /// [spec]: https://tc39.es/proposal-array-find-from-last/#sec-array.prototype.findlast
    pub(crate) fn find_last(
        this: &JsValue,
        args: &[JsValue],
        context: &mut Context,
    ) -> JsResult<JsValue> {
        // 1. Let O be ? ToObject(this value).
        let o = this.to_object(context)?;

        // 2. Let len be ? LengthOfArrayLike(O).
        let len = o.length_of_array_like(context)?;

        // 3. If IsCallable(predicate) is false, throw a TypeError exception.
        let predicate = args.get_or_undefined(0).as_callable().ok_or_else(|| {
            context.construct_type_error("Array.prototype.findLast: predicate is not callable")
        })?;

        let this_arg = args.get_or_undefined(1);

        // 4. Let k be len - 1. (implementation differs slightly from spec because k is unsigned)
        // 5. Repeat, while k >= 0, (implementation differs slightly from spec because k is unsigned)
        for k in (0..len).rev() {
            // a. Let Pk be ! ToString(𝔽(k)).
            // b. Let kValue be ? Get(O, Pk).
            let k_value = o.get(k, context)?;
            // c. Let testResult be ! ToBoolean(? Call(predicate, thisArg, « kValue, 𝔽(k), O »)).
            let test_result = predicate
                .call(
                    this_arg,
                    &[k_value.clone(), k.into(), this.clone()],
                    context,
                )?
                .to_boolean();
            // d. If testResult is true, return kValue.
            if test_result {
                return Ok(k_value);
            }
            // e. Set k to k - 1.
        }
        // 6. Return undefined.
        Ok(JsValue::undefined())
    }

    /// `Array.prototype.findLastIndex( predicate [ , thisArg ] )`
    ///
    /// `findLastIndex` calls predicate once for each element of the array, in descending order,
    /// until it finds one where predicate returns true. If such an element is found, `findLastIndex`
    /// immediately returns the index of that element value. Otherwise, `findLastIndex` returns -1.
    ///
    /// More information:
    ///  - [ECMAScript proposal][spec]
    ///
    /// [spec]: https://tc39.es/proposal-array-find-from-last/#sec-array.prototype.findlastindex
    pub(crate) fn find_last_index(
        this: &JsValue,
        args: &[JsValue],
        context: &mut Context,
    ) -> JsResult<JsValue> {
        // 1. Let O be ? ToObject(this value).
        let o = this.to_object(context)?;

        // 2. Let len be ? LengthOfArrayLike(O).
        let len = o.length_of_array_like(context)?;

        // 3. If IsCallable(predicate) is false, throw a TypeError exception.
        let predicate = args.get_or_undefined(0).as_callable().ok_or_else(|| {
            context.construct_type_error("Array.prototype.findLastIndex: predicate is not callable")
        })?;

        let this_arg = args.get_or_undefined(1);

        // 4. Let k be len - 1. (implementation differs slightly from spec because k is unsigned)
        // 5. Repeat, while k >= 0, (implementation differs slightly from spec because k is unsigned)
        for k in (0..len).rev() {
            // a. Let Pk be ! ToString(𝔽(k)).
            // b. Let kValue be ? Get(O, Pk).
            let k_value = o.get(k, context)?;
            // c. Let testResult be ! ToBoolean(? Call(predicate, thisArg, « kValue, 𝔽(k), O »)).
            let test_result = predicate
                .call(this_arg, &[k_value, k.into(), this.clone()], context)?
                .to_boolean();
            // d. If testResult is true, return 𝔽(k).
            if test_result {
                return Ok(JsValue::new(k));
            }
            // e. Set k to k - 1.
        }
        // 6. Return -1𝔽.
        Ok(JsValue::new(-1))
    }

    /// `Array.prototype.flat( [depth] )`
    ///
    /// This method creates a new array with all sub-array elements concatenated into it
    /// recursively up to the specified depth.
    ///
    /// More information:
    ///  - [ECMAScript reference][spec]
    ///  - [MDN documentation][mdn]
    ///
    /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.flat
    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/flat
    pub(crate) fn flat(
        this: &JsValue,
        args: &[JsValue],
        context: &mut Context,
    ) -> JsResult<JsValue> {
        // 1. Let O be ToObject(this value)
        let o = this.to_object(context)?;

        // 2. Let sourceLen be LengthOfArrayLike(O)
        let source_len = o.length_of_array_like(context)?;

        // 3. Let depthNum be 1
        let mut depth_num = 1;

        // 4. If depth is not undefined, then set depthNum to IntegerOrInfinity(depth)
        if let Some(depth) = args.get(0) {
            // a. Set depthNum to ? ToIntegerOrInfinity(depth).
            // b. If depthNum < 0, set depthNum to 0.
            match depth.to_integer_or_infinity(context)? {
                IntegerOrInfinity::Integer(value) if value >= 0 => depth_num = value as u64,
                IntegerOrInfinity::PositiveInfinity => depth_num = u64::MAX,
                _ => depth_num = 0,
            }
        };

        // 5. Let A be ArraySpeciesCreate(O, 0)
        let a = Self::array_species_create(&o, 0, context)?;

        // 6. Perform ? FlattenIntoArray(A, O, sourceLen, 0, depthNum)
        Self::flatten_into_array(
            &a,
            &o,
            source_len as u64,
            0,
            depth_num,
            None,
            &JsValue::undefined(),
            context,
        )?;

        Ok(a.into())
    }

    /// `Array.prototype.flatMap( callback, [ thisArg ] )`
    ///
    /// This method returns a new array formed by applying a given callback function to
    /// each element of the array, and then flattening the result by one level. It is
    /// identical to a `map()` followed by a `flat()` of depth 1, but slightly more
    /// efficient than calling those two methods separately.
    ///
    /// More information:
    ///  - [ECMAScript reference][spec]
    ///  - [MDN documentation][mdn]
    ///
    /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.flatMap
    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/flatMap
    pub(crate) fn flat_map(
        this: &JsValue,
        args: &[JsValue],
        context: &mut Context,
    ) -> JsResult<JsValue> {
        // 1. Let O be ToObject(this value)
        let o = this.to_object(context)?;

        // 2. Let sourceLen be LengthOfArrayLike(O)
        let source_len = o.length_of_array_like(context)?;

        // 3. If ! IsCallable(mapperFunction) is false, throw a TypeError exception.
        let mapper_function = args.get_or_undefined(0).as_callable().ok_or_else(|| {
            context.construct_type_error("flatMap mapper function is not callable")
        })?;

        // 4. Let A be ? ArraySpeciesCreate(O, 0).
        let a = Self::array_species_create(&o, 0, context)?;

        // 5. Perform ? FlattenIntoArray(A, O, sourceLen, 0, 1, mapperFunction, thisArg).
        Self::flatten_into_array(
            &a,
            &o,
            source_len as u64,
            0,
            1,
            Some(mapper_function),
            args.get_or_undefined(1),
            context,
        )?;

        // 6. Return A
        Ok(a.into())
    }

    /// Abstract method `FlattenIntoArray`.
    ///
    /// More information:
    ///  - [ECMAScript reference][spec]
    ///
    /// [spec]: https://tc39.es/ecma262/#sec-flattenintoarray
    #[allow(clippy::too_many_arguments)]
    fn flatten_into_array(
        target: &JsObject,
        source: &JsObject,
        source_len: u64,
        start: u64,
        depth: u64,
        mapper_function: Option<&JsObject>,
        this_arg: &JsValue,
        context: &mut Context,
    ) -> JsResult<u64> {
        // 1. Assert target is Object
        // 2. Assert source is Object

        // 3. Assert if mapper_function is present, then:
        // - IsCallable(mapper_function) is true
        // - thisArg is present
        // - depth is 1

        // 4. Let targetIndex be start
        let mut target_index = start;

        // 5. Let sourceIndex be 0
        let mut source_index = 0;

        // 6. Repeat, while R(sourceIndex) < sourceLen
        while source_index < source_len {
            // a. Let P be ToString(sourceIndex)
            let p = source_index;

            // b. Let exists be ? HasProperty(source, P).
            let exists = source.has_property(p, context)?;
            // c. If exists is true, then
            if exists {
                // i. Let element be Get(source, P)
                let mut element = source.get(p, context)?;

                // ii. If mapperFunction is present, then
                if let Some(mapper_function) = mapper_function {
                    // 1. Set element to ? Call(mapperFunction, thisArg, <<element, sourceIndex, source>>)
                    element = mapper_function.call(
                        this_arg,
                        &[element, source_index.into(), source.clone().into()],
                        context,
                    )?;
                }

                // iii. Let shouldFlatten be false
                let mut should_flatten = false;

                // iv. If depth > 0, then
                if depth > 0 {
                    // 1. Set shouldFlatten to ? IsArray(element).
                    should_flatten = element.is_array(context)?;
                }

                // v. If shouldFlatten is true
                if should_flatten {
                    // For `should_flatten` to be true, element must be an object.
                    let element = element.as_object().expect("must be an object");

                    // 1. If depth is +Infinity let newDepth be +Infinity
                    let new_depth = if depth == u64::MAX {
                        u64::MAX
                    // 2. Else, let newDepth be depth - 1
                    } else {
                        depth - 1
                    };

                    // 3. Let elementLen be ? LengthOfArrayLike(element)
                    let element_len = element.length_of_array_like(context)?;

                    // 4. Set targetIndex to ? FlattenIntoArray(target, element, elementLen, targetIndex, newDepth)
                    target_index = Self::flatten_into_array(
                        target,
                        element,
                        element_len as u64,
                        target_index,
                        new_depth,
                        None,
                        &JsValue::undefined(),
                        context,
                    )?;

                // vi. Else
                } else {
                    // 1. If targetIndex >= 2^53 - 1, throw a TypeError exception
                    if target_index >= Number::MAX_SAFE_INTEGER as u64 {
                        return context
                            .throw_type_error("Target index exceeded max safe integer value");
                    }

                    // 2. Perform ? CreateDataPropertyOrThrow(target, targetIndex, element)
                    target.create_data_property_or_throw(target_index, element, context)?;

                    // 3. Set targetIndex to targetIndex + 1
                    target_index += 1;
                }
            }
            // d. Set sourceIndex to sourceIndex + 1
            source_index += 1;
        }

        // 7. Return targetIndex
        Ok(target_index)
    }

    /// `Array.prototype.fill( value[, start[, end]] )`
    ///
    /// The method fills (modifies) all the elements of an array from start index (default 0)
    /// to an end index (default array length) with a static value. It returns the modified array.
    ///
    /// More information:
    ///  - [ECMAScript reference][spec]
    ///  - [MDN documentation][mdn]
    ///
    /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.fill
    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/fill
    pub(crate) fn fill(
        this: &JsValue,
        args: &[JsValue],
        context: &mut Context,
    ) -> JsResult<JsValue> {
        // 1. Let O be ? ToObject(this value).
        let o = this.to_object(context)?;

        // 2. Let len be ? LengthOfArrayLike(O).
        let len = o.length_of_array_like(context)?;

        // 3. Let relativeStart be ? ToIntegerOrInfinity(start).
        // 4. If relativeStart is -∞, let k be 0.
        // 5. Else if relativeStart < 0, let k be max(len + relativeStart, 0).
        // 6. Else, let k be min(relativeStart, len).
        let mut k = Self::get_relative_start(context, args.get(1), len)?;

        // 7. If end is undefined, let relativeEnd be len; else let relativeEnd be ? ToIntegerOrInfinity(end).
        // 8. If relativeEnd is -∞, let final be 0.
        // 9. Else if relativeEnd < 0, let final be max(len + relativeEnd, 0).
        // 10. Else, let final be min(relativeEnd, len).
        let final_ = Self::get_relative_end(context, args.get(2), len)?;

        let value = args.get_or_undefined(0);

        // 11. Repeat, while k < final,
        while k < final_ {
            // a. Let Pk be ! ToString(𝔽(k)).
            let pk = k;
            // b. Perform ? Set(O, Pk, value, true).
            o.set(pk, value.clone(), true, context)?;
            // c. Set k to k + 1.
            k += 1;
        }
        // 12. Return O.
        Ok(o.into())
    }

    /// `Array.prototype.includes( valueToFind [, fromIndex] )`
    ///
    /// Determines whether an array includes a certain value among its entries, returning `true` or `false` as appropriate.
    ///
    /// More information:
    ///  - [ECMAScript reference][spec]
    ///  - [MDN documentation][mdn]
    ///
    /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.includes
    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/includes
    pub(crate) fn includes_value(
        this: &JsValue,
        args: &[JsValue],
        context: &mut Context,
    ) -> JsResult<JsValue> {
        // 1. Let O be ? ToObject(this value).
        let o = this.to_object(context)?;

        // 2. Let len be ? LengthOfArrayLike(O).
        let len = o.length_of_array_like(context)? as i64;

        // 3. If len is 0, return false.
        if len == 0 {
            return Ok(JsValue::new(false));
        }

        // 4. Let n be ? ToIntegerOrInfinity(fromIndex).
        let n = args
            .get(1)
            .cloned()
            .unwrap_or_default()
            .to_integer_or_infinity(context)?;
        // 5. Assert: If fromIndex is undefined, then n is 0.
        // 6. If n is +∞, return false.
        // 7. Else if n is -∞, set n to 0.
        let n = match n {
            IntegerOrInfinity::PositiveInfinity => return Ok(JsValue::new(false)),
            IntegerOrInfinity::NegativeInfinity => 0,
            IntegerOrInfinity::Integer(value) => value,
        };

        // 8. If n ≥ 0, then
        let mut k;
        if n >= 0 {
            // a. Let k be n.
            k = n;
        // 9. Else,
        } else {
            // a. Let k be len + n.
            k = len + n;
            // b. If k < 0, set k to 0.
            if k < 0 {
                k = 0;
            }
        }

        let search_element = args.get_or_undefined(0);

        // 10. Repeat, while k < len,
        while k < len {
            // a. Let elementK be ? Get(O, ! ToString(𝔽(k))).
            let element_k = o.get(k, context)?;
            // b. If SameValueZero(searchElement, elementK) is true, return true.
            if JsValue::same_value_zero(search_element, &element_k) {
                return Ok(JsValue::new(true));
            }
            // c. Set k to k + 1.
            k += 1;
        }
        // 11. Return false.
        Ok(JsValue::new(false))
    }

    /// `Array.prototype.slice( [begin[, end]] )`
    ///
    /// The slice method takes two arguments, start and end, and returns an array containing the
    /// elements of the array from element start up to, but not including, element end (or through the
    /// end of the array if end is undefined). If start is negative, it is treated as length + start
    /// where length is the length of the array. If end is negative, it is treated as length + end where
    /// length is the length of the array.
    ///
    /// More information:
    ///  - [ECMAScript reference][spec]
    ///  - [MDN documentation][mdn]
    ///
    /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.slice
    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/slice
    pub(crate) fn slice(
        this: &JsValue,
        args: &[JsValue],
        context: &mut Context,
    ) -> JsResult<JsValue> {
        // 1. Let O be ? ToObject(this value).
        let o = this.to_object(context)?;

        // 2. Let len be ? LengthOfArrayLike(O).
        let len = o.length_of_array_like(context)?;

        // 3. Let relativeStart be ? ToIntegerOrInfinity(start).
        // 4. If relativeStart is -∞, let k be 0.
        // 5. Else if relativeStart < 0, let k be max(len + relativeStart, 0).
        // 6. Else, let k be min(relativeStart, len).
        let mut k = Self::get_relative_start(context, args.get(0), len)?;

        // 7. If end is undefined, let relativeEnd be len; else let relativeEnd be ? ToIntegerOrInfinity(end).
        // 8. If relativeEnd is -∞, let final be 0.
        // 9. Else if relativeEnd < 0, let final be max(len + relativeEnd, 0).
        // 10. Else, let final be min(relativeEnd, len).
        let final_ = Self::get_relative_end(context, args.get(1), len)?;

        // 11. Let count be max(final - k, 0).
        let count = final_.saturating_sub(k);

        // 12. Let A be ? ArraySpeciesCreate(O, count).
        let a = Self::array_species_create(&o, count, context)?;

        // 13. Let n be 0.
        let mut n: u64 = 0;
        // 14. Repeat, while k < final,
        while k < final_ {
            // a. Let Pk be ! ToString(𝔽(k)).
            let pk = k;
            // b. Let kPresent be ? HasProperty(O, Pk).
            let k_present = o.has_property(pk, context)?;
            // c. If kPresent is true, then
            if k_present {
                // i. Let kValue be ? Get(O, Pk).
                let k_value = o.get(pk, context)?;
                // ii. Perform ? CreateDataPropertyOrThrow(A, ! ToString(𝔽(n)), kValue).
                a.create_data_property_or_throw(n, k_value, context)?;
            }
            // d. Set k to k + 1.
            k += 1;
            // e. Set n to n + 1.
            n += 1;
        }

        // 15. Perform ? Set(A, "length", 𝔽(n), true).
        a.set("length", n, true, context)?;

        // 16. Return A.
        Ok(a.into())
    }

    /// `Array.prototype.splice ( start, [deleteCount[, ...items]] )`
    ///
    /// Splices an array by following
    /// The deleteCount elements of the array starting at integer index start are replaced by the elements of items.
    /// An Array object containing the deleted elements (if any) is returned.
    pub(crate) fn splice(
        this: &JsValue,
        args: &[JsValue],
        context: &mut Context,
    ) -> JsResult<JsValue> {
        // 1. Let O be ? ToObject(this value).
        let o = this.to_object(context)?;
        // 2. Let len be ? LengthOfArrayLike(O).
        let len = o.length_of_array_like(context)?;

        let start = args.get(0);
        let delete_count = args.get(1);
        let items = args.get(2..).unwrap_or(&[]);
        // 3. Let relativeStart be ? ToIntegerOrInfinity(start).
        // 4. If relativeStart is -∞, let actualStart be 0.
        // 5. Else if relativeStart < 0, let actualStart be max(len + relativeStart, 0).
        // 6. Else, let actualStart be min(relativeStart, len).
        let actual_start = Self::get_relative_start(context, start, len)?;
        // 7. If start is not present, then
        let insert_count = if start.is_none() || delete_count.is_none() {
            // 7a. Let insertCount be 0.
            // 8. Else if deleteCount is not present, then
            // a. Let insertCount be 0.
            0
        // 9. Else,
        } else {
            // 9a. Let insertCount be the number of elements in items.
            items.len()
        };
        let actual_delete_count = if start.is_none() {
            // 7b. Let actualDeleteCount be 0.
            0
            // 8. Else if deleteCount is not present, then
        } else if delete_count.is_none() {
            // 8b. Let actualDeleteCount be len - actualStart.
            len - actual_start
        // 9. Else,
        } else {
            // b. Let dc be ? ToIntegerOrInfinity(deleteCount).
            let dc = delete_count
                .cloned()
                .unwrap_or_default()
                .to_integer_or_infinity(context)?;
            // c. Let actualDeleteCount be the result of clamping dc between 0 and len - actualStart.
            let max = len - actual_start;
            match dc {
                IntegerOrInfinity::Integer(i) => {
                    usize::try_from(i).unwrap_or_default().clamp(0, max)
                }
                IntegerOrInfinity::PositiveInfinity => max,
                IntegerOrInfinity::NegativeInfinity => 0,
            }
        };

        // 10. If len + insertCount - actualDeleteCount > 2^53 - 1, throw a TypeError exception.
        if len + insert_count - actual_delete_count > Number::MAX_SAFE_INTEGER as usize {
            return context.throw_type_error("Target splice exceeded max safe integer value");
        }

        // 11. Let A be ? ArraySpeciesCreate(O, actualDeleteCount).
        let arr = Self::array_species_create(&o, actual_delete_count, context)?;
        // 12. Let k be 0.
        // 13. Repeat, while k < actualDeleteCount,
        for k in 0..actual_delete_count {
            // a. Let from be ! ToString(𝔽(actualStart + k)).
            // b. Let fromPresent be ? HasProperty(O, from).
            let from_present = o.has_property(actual_start + k, context)?;
            // c. If fromPresent is true, then
            if from_present {
                // i. Let fromValue be ? Get(O, from).
                let from_value = o.get(actual_start + k, context)?;
                // ii. Perform ? CreateDataPropertyOrThrow(A, ! ToString(𝔽(k)), fromValue).
                arr.create_data_property_or_throw(k, from_value, context)?;
            }
            // d. Set k to k + 1.
        }

        // 14. Perform ? Set(A, "length", 𝔽(actualDeleteCount), true).
        arr.set("length", actual_delete_count, true, context)?;

        // 15. Let itemCount be the number of elements in items.
        let item_count = items.len();

        match item_count.cmp(&actual_delete_count) {
            // 16. If itemCount < actualDeleteCount, then
            Ordering::Less => {
                //     a. Set k to actualStart.
                //     b. Repeat, while k < (len - actualDeleteCount),
                for k in actual_start..(len - actual_delete_count) {
                    // i. Let from be ! ToString(𝔽(k + actualDeleteCount)).
                    let from = k + actual_delete_count;
                    // ii. Let to be ! ToString(𝔽(k + itemCount)).
                    let to = k + item_count;
                    // iii. Let fromPresent be ? HasProperty(O, from).
                    let from_present = o.has_property(from, context)?;
                    // iv. If fromPresent is true, then
                    if from_present {
                        // 1. Let fromValue be ? Get(O, from).
                        let from_value = o.get(from, context)?;
                        // 2. Perform ? Set(O, to, fromValue, true).
                        o.set(to, from_value, true, context)?;
                    // v. Else,
                    } else {
                        // 1. Assert: fromPresent is false.
                        debug_assert!(!from_present);
                        // 2. Perform ? DeletePropertyOrThrow(O, to).
                        o.delete_property_or_throw(to, context)?;
                    }
                    // vi. Set k to k + 1.
                }
                // c. Set k to len.
                // d. Repeat, while k > (len - actualDeleteCount + itemCount),
                for k in ((len - actual_delete_count + item_count)..len).rev() {
                    // i. Perform ? DeletePropertyOrThrow(O, ! ToString(𝔽(k - 1))).
                    o.delete_property_or_throw(k, context)?;
                    // ii. Set k to k - 1.
                }
            }
            // 17. Else if itemCount > actualDeleteCount, then
            Ordering::Greater => {
                // a. Set k to (len - actualDeleteCount).
                // b. Repeat, while k > actualStart,
                for k in (actual_start..len - actual_delete_count).rev() {
                    // i. Let from be ! ToString(𝔽(k + actualDeleteCount - 1)).
                    let from = k + actual_delete_count;
                    // ii. Let to be ! ToString(𝔽(k + itemCount - 1)).
                    let to = k + item_count;
                    // iii. Let fromPresent be ? HasProperty(O, from).
                    let from_present = o.has_property(from, context)?;
                    // iv. If fromPresent is true, then
                    if from_present {
                        // 1. Let fromValue be ? Get(O, from).
                        let from_value = o.get(from, context)?;
                        // 2. Perform ? Set(O, to, fromValue, true).
                        o.set(to, from_value, true, context)?;
                    // v. Else,
                    } else {
                        // 1. Assert: fromPresent is false.
                        debug_assert!(!from_present);
                        // 2. Perform ? DeletePropertyOrThrow(O, to).
                        o.delete_property_or_throw(to, context)?;
                    }
                    // vi. Set k to k - 1.
                }
            }
            Ordering::Equal => {}
        };

        // 18. Set k to actualStart.
        // 19. For each element E of items, do
        if item_count > 0 {
            for (k, item) in items
                .iter()
                .enumerate()
                .map(|(i, val)| (i + actual_start, val))
            {
                // a. Perform ? Set(O, ! ToString(𝔽(k)), E, true).
                o.set(k, item, true, context)?;
                // b. Set k to k + 1.
            }
        }

        // 20. Perform ? Set(O, "length", 𝔽(len - actualDeleteCount + itemCount), true).
        o.set(
            "length",
            len - actual_delete_count + item_count,
            true,
            context,
        )?;

        // 21. Return A.
        Ok(JsValue::from(arr))
    }

    /// `Array.prototype.filter( callback, [ thisArg ] )`
    ///
    /// For each element in the array the callback function is called, and a new
    /// array is constructed for every value whose callback returned a truthy value.
    ///
    /// More information:
    ///  - [ECMAScript reference][spec]
    ///  - [MDN documentation][mdn]
    ///
    /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.filter
    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/filter
    pub(crate) fn filter(
        this: &JsValue,
        args: &[JsValue],
        context: &mut Context,
    ) -> JsResult<JsValue> {
        // 1. Let O be ? ToObject(this value).
        let o = this.to_object(context)?;

        // 2. Let len be ? LengthOfArrayLike(O).
        let length = o.length_of_array_like(context)?;

        // 3. If IsCallable(callbackfn) is false, throw a TypeError exception.
        let callback = args.get_or_undefined(0).as_callable().ok_or_else(|| {
            context.construct_type_error("Array.prototype.filter: `callback` must be callable")
        })?;
        let this_arg = args.get_or_undefined(1);

        // 4. Let A be ? ArraySpeciesCreate(O, 0).
        let a = Self::array_species_create(&o, 0, context)?;

        // 5. Let k be 0.
        // 6. Let to be 0.
        let mut to = 0u32;
        // 7. Repeat, while k < len,
        for idx in 0..length {
            // a. Let Pk be ! ToString(𝔽(k)).
            // b. Let kPresent be ? HasProperty(O, Pk).
            // c. If kPresent is true, then
            if o.has_property(idx, context)? {
                // i. Let kValue be ? Get(O, Pk).
                let element = o.get(idx, context)?;

                let args = [element.clone(), JsValue::new(idx), JsValue::new(o.clone())];

                // ii. Let selected be ! ToBoolean(? Call(callbackfn, thisArg, « kValue, 𝔽(k), O »)).
                let selected = callback.call(this_arg, &args, context)?.to_boolean();

                // iii. If selected is true, then
                if selected {
                    // 1. Perform ? CreateDataPropertyOrThrow(A, ! ToString(𝔽(to)), kValue).
                    a.create_data_property_or_throw(to, element, context)?;
                    // 2. Set to to to + 1.
                    to += 1;
                }
            }
        }

        // 8. Return A.
        Ok(a.into())
    }

    /// Array.prototype.some ( callbackfn [ , thisArg ] )
    ///
    /// The some method tests whether at least one element in the array passes
    /// the test implemented by the provided callback function. It returns a Boolean value,
    /// true if the callback function returns a truthy value for at least one element
    /// in the array. Otherwise, false.
    ///
    /// Caution: Calling this method on an empty array returns false for any condition!
    ///
    /// More information:
    ///  - [ECMAScript reference][spec]
    ///  - [MDN documentation][mdn]
    ///
    /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.some
    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/some
    pub(crate) fn some(
        this: &JsValue,
        args: &[JsValue],
        context: &mut Context,
    ) -> JsResult<JsValue> {
        // 1. Let O be ? ToObject(this value).
        let o = this.to_object(context)?;
        // 2. Let len be ? LengthOfArrayLike(O).
        let len = o.length_of_array_like(context)?;
        // 3. If IsCallable(callbackfn) is false, throw a TypeError exception.
        let callback = args.get_or_undefined(0).as_callable().ok_or_else(|| {
            context.construct_type_error("Array.prototype.some: callback is not callable")
        })?;

        // 4. Let k be 0.
        // 5. Repeat, while k < len,
        for k in 0..len {
            // a. Let Pk be ! ToString(𝔽(k)).
            // b. Let kPresent be ? HasProperty(O, Pk).
            let k_present = o.has_property(k, context)?;
            // c. If kPresent is true, then
            if k_present {
                // i. Let kValue be ? Get(O, Pk).
                let k_value = o.get(k, context)?;
                // ii. Let testResult be ! ToBoolean(? Call(callbackfn, thisArg, « kValue, 𝔽(k), O »)).
                let this_arg = args.get_or_undefined(1);
                let test_result = callback
                    .call(this_arg, &[k_value, k.into(), o.clone().into()], context)?
                    .to_boolean();
                // iii. If testResult is true, return true.
                if test_result {
                    return Ok(JsValue::new(true));
                }
            }
            // d. Set k to k + 1.
        }
        // 6. Return false.
        Ok(JsValue::new(false))
    }

    /// Array.prototype.sort ( comparefn )
    ///
    /// The sort method sorts the elements of an array in place and returns the sorted array.
    /// The default sort order is ascending, built upon converting the elements into strings,
    /// then comparing their sequences of UTF-16 code units values.
    ///
    /// More information:
    ///  - [ECMAScript reference][spec]
    ///  - [MDN documentation][mdn]
    ///
    /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.sort
    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort
    pub(crate) fn sort(
        this: &JsValue,
        args: &[JsValue],
        context: &mut Context,
    ) -> JsResult<JsValue> {
        // 1. If comparefn is not undefined and IsCallable(comparefn) is false, throw a TypeError exception.
        let comparefn = match args.get_or_undefined(0) {
            JsValue::Object(ref obj) if obj.is_callable() => Some(obj),
            JsValue::Undefined => None,
            _ => {
                return context.throw_type_error(
                    "The comparison function must be either a function or undefined",
                )
            }
        };

        // Abstract method `SortCompare`.
        //
        // More information:
        //  - [ECMAScript reference][spec]
        //
        // [spec]: https://tc39.es/ecma262/#sec-sortcompare
        let sort_compare =
            |x: &JsValue, y: &JsValue, context: &mut Context| -> JsResult<Ordering> {
                match (x.is_undefined(), y.is_undefined()) {
                    // 1. If x and y are both undefined, return +0𝔽.
                    (true, true) => return Ok(Ordering::Equal),
                    // 2. If x is undefined, return 1𝔽.
                    (true, false) => return Ok(Ordering::Greater),
                    // 3. If y is undefined, return -1𝔽.
                    (false, true) => return Ok(Ordering::Less),
                    _ => {}
                }

                // 4. If comparefn is not undefined, then
                if let Some(cmp) = comparefn {
                    let args = [x.clone(), y.clone()];
                    // a. Let v be ? ToNumber(? Call(comparefn, undefined, « x, y »)).
                    let v = cmp
                        .call(&JsValue::Undefined, &args, context)?
                        .to_number(context)?;
                    // b. If v is NaN, return +0𝔽.
                    // c. Return v.
                    return Ok(v.partial_cmp(&0.0).unwrap_or(Ordering::Equal));
                }
                // 5. Let xString be ? ToString(x).
                // 6. Let yString be ? ToString(y).
                let x_str = x.to_string(context)?;
                let y_str = y.to_string(context)?;

                // 7. Let xSmaller be IsLessThan(xString, yString, true).
                // 8. If xSmaller is true, return -1𝔽.
                // 9. Let ySmaller be IsLessThan(yString, xString, true).
                // 10. If ySmaller is true, return 1𝔽.
                // 11. Return +0𝔽.

                // NOTE: skipped IsLessThan because it just makes a lexicographic comparison
                // when x and y are strings
                Ok(x_str.cmp(&y_str))
            };

        // 2. Let obj be ? ToObject(this value).
        let obj = this.to_object(context)?;

        // 3. Let len be ? LengthOfArrayLike(obj).
        let length = obj.length_of_array_like(context)?;

        // 4. Let items be a new empty List.
        let mut items = Vec::with_capacity(length);

        // 5. Let k be 0.
        // 6. Repeat, while k < len,
        for k in 0..length {
            // a. Let Pk be ! ToString(𝔽(k)).
            // b. Let kPresent be ? HasProperty(obj, Pk).
            // c. If kPresent is true, then
            if obj.has_property(k, context)? {
                // i. Let kValue be ? Get(obj, Pk).
                let kval = obj.get(k, context)?;
                // ii. Append kValue to items.
                items.push(kval);
            }
            // d. Set k to k + 1.
        }

        // 7. Let itemCount be the number of elements in items.
        let item_count = items.len();

        // 8. Sort items using an implementation-defined sequence of calls to SortCompare.
        // If any such call returns an abrupt completion, stop before performing any further
        // calls to SortCompare or steps in this algorithm and return that completion.
        let mut sort_err = Ok(());
        items.sort_by(|x, y| {
            if sort_err.is_ok() {
                sort_compare(x, y, context).unwrap_or_else(|err| {
                    sort_err = Err(err);
                    Ordering::Equal
                })
            } else {
                Ordering::Equal
            }
        });
        sort_err?;

        // 9. Let j be 0.
        // 10. Repeat, while j < itemCount,
        for (j, item) in items.into_iter().enumerate() {
            // a. Perform ? Set(obj, ! ToString(𝔽(j)), items[j], true).
            obj.set(j, item, true, context)?;
            // b. Set j to j + 1.
        }

        // 11. Repeat, while j < len,
        for j in item_count..length {
            // a. Perform ? DeletePropertyOrThrow(obj, ! ToString(𝔽(j))).
            obj.delete_property_or_throw(j, context)?;
            // b. Set j to j + 1.
        }

        // 12. Return obj.
        Ok(obj.into())
    }

    /// `Array.prototype.reduce( callbackFn [ , initialValue ] )`
    ///
    /// More information:
    ///  - [ECMAScript reference][spec]
    ///  - [MDN documentation][mdn]
    ///
    /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.reduce
    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce
    pub(crate) fn reduce(
        this: &JsValue,
        args: &[JsValue],
        context: &mut Context,
    ) -> JsResult<JsValue> {
        // 1. Let O be ? ToObject(this value).
        let o = this.to_object(context)?;

        // 2. Let len be ? LengthOfArrayLike(O).
        let len = o.length_of_array_like(context)?;

        // 3. If IsCallable(callbackfn) is false, throw a TypeError exception.
        let callback = args.get_or_undefined(0).as_callable().ok_or_else(|| {
            context
                .construct_type_error("Array.prototype.reduce: callback function is not callable")
        })?;

        // 4. If len = 0 and initialValue is not present, throw a TypeError exception.
        if len == 0 && args.get(1).is_none() {
            return context.throw_type_error(
                "Array.prototype.reduce: called on an empty array and with no initial value",
            );
        }

        // 5. Let k be 0.
        let mut k = 0;
        // 6. Let accumulator be undefined.
        let mut accumulator = JsValue::undefined();

        // 7. If initialValue is present, then
        if let Some(initial_value) = args.get(1) {
            // a. Set accumulator to initialValue.
            accumulator = initial_value.clone();
        // 8. Else,
        } else {
            // a. Let kPresent be false.
            let mut k_present = false;
            // b. Repeat, while kPresent is false and k < len,
            while !k_present && k < len {
                // i. Let Pk be ! ToString(𝔽(k)).
                let pk = k;
                // ii. Set kPresent to ? HasProperty(O, Pk).
                k_present = o.has_property(pk, context)?;
                // iii. If kPresent is true, then
                if k_present {
                    // 1. Set accumulator to ? Get(O, Pk).
                    accumulator = o.get(pk, context)?;
                }
                // iv. Set k to k + 1.
                k += 1;
            }
            // c. If kPresent is false, throw a TypeError exception.
            if !k_present {
                return context.throw_type_error(
                    "Array.prototype.reduce: called on an empty array and with no initial value",
                );
            }
        }

        // 9. Repeat, while k < len,
        while k < len {
            // a. Let Pk be ! ToString(𝔽(k)).
            let pk = k;
            // b. Let kPresent be ? HasProperty(O, Pk).
            let k_present = o.has_property(pk, context)?;
            // c. If kPresent is true, then
            if k_present {
                // i. Let kValue be ? Get(O, Pk).
                let k_value = o.get(pk, context)?;
                // ii. Set accumulator to ? Call(callbackfn, undefined, « accumulator, kValue, 𝔽(k), O »).
                accumulator = callback.call(
                    &JsValue::undefined(),
                    &[accumulator, k_value, k.into(), o.clone().into()],
                    context,
                )?;
            }
            // d. Set k to k + 1.
            k += 1;
        }

        // 10. Return accumulator.
        Ok(accumulator)
    }

    /// `Array.prototype.reduceRight( callbackFn [ , initialValue ] )`
    ///
    /// The reduceRight method traverses right to left starting from the last defined value in the array,
    /// accumulating a value using a given callback function. It returns the accumulated value.
    ///
    /// More information:
    ///  - [ECMAScript reference][spec]
    ///  - [MDN documentation][mdn]
    ///
    /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.reduceright
    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduceRight
    pub(crate) fn reduce_right(
        this: &JsValue,
        args: &[JsValue],
        context: &mut Context,
    ) -> JsResult<JsValue> {
        // 1. Let O be ? ToObject(this value).
        let o = this.to_object(context)?;

        // 2. Let len be ? LengthOfArrayLike(O).
        let len = o.length_of_array_like(context)?;

        // 3. If IsCallable(callbackfn) is false, throw a TypeError exception.
        let callback = args.get_or_undefined(0).as_callable().ok_or_else(|| {
            context.construct_type_error(
                "Array.prototype.reduceRight: callback function is not callable",
            )
        })?;

        // 4. If len is 0 and initialValue is not present, throw a TypeError exception.
        if len == 0 && args.get(1).is_none() {
            return context.throw_type_error(
                "Array.prototype.reduceRight: called on an empty array and with no initial value",
            );
        }

        // 5. Let k be len - 1.
        let mut k = len as i64 - 1;
        // 6. Let accumulator be undefined.
        let mut accumulator = JsValue::undefined();
        // 7. If initialValue is present, then
        if let Some(initial_value) = args.get(1) {
            // a. Set accumulator to initialValue.
            accumulator = initial_value.clone();
        // 8. Else,
        } else {
            // a. Let kPresent be false.
            let mut k_present = false;
            // b. Repeat, while kPresent is false and k ≥ 0,
            while !k_present && k >= 0 {
                // i. Let Pk be ! ToString(𝔽(k)).
                let pk = k;
                // ii. Set kPresent to ? HasProperty(O, Pk).
                k_present = o.has_property(pk, context)?;
                // iii. If kPresent is true, then
                if k_present {
                    // 1. Set accumulator to ? Get(O, Pk).
                    accumulator = o.get(pk, context)?;
                }
                // iv. Set k to k - 1.
                k -= 1;
            }
            // c. If kPresent is false, throw a TypeError exception.
            if !k_present {
                return context.throw_type_error(
                    "Array.prototype.reduceRight: called on an empty array and with no initial value",
                );
            }
        }

        // 9. Repeat, while k ≥ 0,
        while k >= 0 {
            // a. Let Pk be ! ToString(𝔽(k)).
            let pk = k;
            // b. Let kPresent be ? HasProperty(O, Pk).
            let k_present = o.has_property(pk, context)?;
            // c. If kPresent is true, then
            if k_present {
                // i. Let kValue be ? Get(O, Pk).
                let k_value = o.get(pk, context)?;
                // ii. Set accumulator to ? Call(callbackfn, undefined, « accumulator, kValue, 𝔽(k), O »).
                accumulator = callback.call(
                    &JsValue::undefined(),
                    &[accumulator.clone(), k_value, k.into(), o.clone().into()],
                    context,
                )?;
            }
            // d. Set k to k - 1.
            k -= 1;
        }

        // 10. Return accumulator.
        Ok(accumulator)
    }

    /// `Array.prototype.copyWithin ( target, start [ , end ] )`
    ///
    /// The copyWithin() method shallow copies part of an array to another location
    /// in the same array and returns it without modifying its length.
    ///
    /// More information:
    ///  - [ECMAScript reference][spec]
    ///  - [MDN documentation][mdn]
    ///
    /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.copywithin
    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/copyWithin
    pub(crate) fn copy_within(
        this: &JsValue,
        args: &[JsValue],
        context: &mut Context,
    ) -> JsResult<JsValue> {
        // 1. Let O be ? ToObject(this value).
        let o = this.to_object(context)?;

        // 2. Let len be ? LengthOfArrayLike(O).
        let len = o.length_of_array_like(context)?;

        // 3. Let relativeTarget be ? ToIntegerOrInfinity(target).
        // 4. If relativeTarget is -∞, let to be 0.
        // 5. Else if relativeTarget < 0, let to be max(len + relativeTarget, 0).
        // 6. Else, let to be min(relativeTarget, len).
        let mut to = Self::get_relative_start(context, args.get(0), len)? as i64;

        // 7. Let relativeStart be ? ToIntegerOrInfinity(start).
        // 8. If relativeStart is -∞, let from be 0.
        // 9. Else if relativeStart < 0, let from be max(len + relativeStart, 0).
        // 10. Else, let from be min(relativeStart, len).
        let mut from = Self::get_relative_start(context, args.get(1), len)? as i64;

        // 11. If end is undefined, let relativeEnd be len; else let relativeEnd be ? ToIntegerOrInfinity(end).
        // 12. If relativeEnd is -∞, let final be 0.
        // 13. Else if relativeEnd < 0, let final be max(len + relativeEnd, 0).
        // 14. Else, let final be min(relativeEnd, len).
        let final_ = Self::get_relative_end(context, args.get(2), len)? as i64;

        // 15. Let count be min(final - from, len - to).
        let mut count = min(final_ - from, len as i64 - to);

        // 16. If from < to and to < from + count, then
        let direction = if from < to && to < from + count {
            // b. Set from to from + count - 1.
            from = from + count - 1;
            // c. Set to to to + count - 1.
            to = to + count - 1;

            // a. Let direction be -1.
            -1
        // 17. Else,
        } else {
            // a. Let direction be 1.
            1
        };

        // 18. Repeat, while count > 0,
        while count > 0 {
            // a. Let fromKey be ! ToString(𝔽(from)).
            let from_key = from;

            // b. Let toKey be ! ToString(𝔽(to)).
            let to_key = to;

            // c. Let fromPresent be ? HasProperty(O, fromKey).
            let from_present = o.has_property(from_key, context)?;
            // d. If fromPresent is true, then
            if from_present {
                // i. Let fromVal be ? Get(O, fromKey).
                let from_val = o.get(from_key, context)?;
                // ii. Perform ? Set(O, toKey, fromVal, true).
                o.set(to_key, from_val, true, context)?;
            // e. Else,
            } else {
                // i. Assert: fromPresent is false.
                // ii. Perform ? DeletePropertyOrThrow(O, toKey).
                o.delete_property_or_throw(to_key, context)?;
            }
            // f. Set from to from + direction.
            from += direction;
            // g. Set to to to + direction.
            to += direction;
            // h. Set count to count - 1.
            count -= 1;
        }
        // 19. Return O.
        Ok(o.into())
    }

    /// `Array.prototype.values( )`
    ///
    /// The values method returns an iterable that iterates over the values in the array.
    ///
    /// More information:
    ///  - [ECMAScript reference][spec]
    ///  - [MDN documentation][mdn]
    ///
    /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.values
    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/values
    pub(crate) fn values(
        this: &JsValue,
        _: &[JsValue],
        context: &mut Context,
    ) -> JsResult<JsValue> {
        // 1. Let O be ? ToObject(this value).
        let o = this.to_object(context)?;

        // 2. Return CreateArrayIterator(O, value).
        Ok(ArrayIterator::create_array_iterator(
            o,
            PropertyNameKind::Value,
            context,
        ))
    }

    /// `Array.prototype.keys( )`
    ///
    /// The keys method returns an iterable that iterates over the indexes in the array.
    ///
    /// More information:
    ///  - [ECMAScript reference][spec]
    ///  - [MDN documentation][mdn]
    ///
    /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.keys
    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/values
    pub(crate) fn keys(this: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
        // 1. Let O be ? ToObject(this value).
        let o = this.to_object(context)?;

        // 2. Return CreateArrayIterator(O, key).
        Ok(ArrayIterator::create_array_iterator(
            o,
            PropertyNameKind::Key,
            context,
        ))
    }

    /// `Array.prototype.entries( )`
    ///
    /// The entries method returns an iterable that iterates over the key-value pairs in the array.
    ///
    /// More information:
    ///  - [ECMAScript reference][spec]
    ///  - [MDN documentation][mdn]
    ///
    /// [spec]: https://tc39.es/ecma262/#sec-array.prototype.entries
    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/values
    pub(crate) fn entries(
        this: &JsValue,
        _: &[JsValue],
        context: &mut Context,
    ) -> JsResult<JsValue> {
        // 1. Let O be ? ToObject(this value).
        let o = this.to_object(context)?;

        // 2. Return CreateArrayIterator(O, key+value).
        Ok(ArrayIterator::create_array_iterator(
            o,
            PropertyNameKind::KeyAndValue,
            context,
        ))
    }

    /// Represents the algorithm to calculate `relativeStart` (or `k`) in array functions.
    pub(super) fn get_relative_start(
        context: &mut Context,
        arg: Option<&JsValue>,
        len: usize,
    ) -> JsResult<usize> {
        // 1. Let relativeStart be ? ToIntegerOrInfinity(start).
        let relative_start = arg
            .cloned()
            .unwrap_or_default()
            .to_integer_or_infinity(context)?;
        match relative_start {
            // 2. If relativeStart is -∞, let k be 0.
            IntegerOrInfinity::NegativeInfinity => Ok(0),
            // 3. Else if relativeStart < 0, let k be max(len + relativeStart, 0).
            IntegerOrInfinity::Integer(i) if i < 0 => Ok(max(len as i64 + i, 0) as usize),
            // Both `as` casts are safe as both variables are non-negative
            // 4. Else, let k be min(relativeStart, len).
            IntegerOrInfinity::Integer(i) => Ok(min(i, len as i64) as usize),

            // Special case - positive infinity. `len` is always smaller than +inf, thus from (4)
            IntegerOrInfinity::PositiveInfinity => Ok(len),
        }
    }

    /// Represents the algorithm to calculate `relativeEnd` (or `final`) in array functions.
    pub(super) fn get_relative_end(
        context: &mut Context,
        arg: Option<&JsValue>,
        len: usize,
    ) -> JsResult<usize> {
        let default_value = JsValue::undefined();
        let value = arg.unwrap_or(&default_value);
        // 1. If end is undefined, let relativeEnd be len [and return it]
        if value.is_undefined() {
            Ok(len)
        } else {
            // 1. cont, else let relativeEnd be ? ToIntegerOrInfinity(end).
            let relative_end = value.to_integer_or_infinity(context)?;
            match relative_end {
                // 2. If relativeEnd is -∞, let final be 0.
                IntegerOrInfinity::NegativeInfinity => Ok(0),
                // 3. Else if relativeEnd < 0, let final be max(len + relativeEnd, 0).
                IntegerOrInfinity::Integer(i) if i < 0 => Ok(max(len as i64 + i, 0) as usize),
                // 4. Else, let final be min(relativeEnd, len).
                // Both `as` casts are safe as both variables are non-negative
                IntegerOrInfinity::Integer(i) => Ok(min(i, len as i64) as usize),

                // Special case - positive infinity. `len` is always smaller than +inf, thus from (4)
                IntegerOrInfinity::PositiveInfinity => Ok(len),
            }
        }
    }

    pub(crate) fn values_intrinsic(context: &mut Context) -> JsFunction {
        FunctionBuilder::native(context, Self::values)
            .name("values")
            .length(0)
            .constructor(false)
            .build()
    }

    /// `Array.prototype [ @@unscopables ]`
    ///
    /// The initial value of the 'unscopables' data property is an ordinary object
    /// with the following boolean properties set to true:
    /// 'at', 'copyWithin', 'entries', 'fill', 'find', 'findIndex', 'flat',
    /// 'flatMap', 'includes', 'keys', 'values'
    ///
    /// More information:
    ///  - [ECMAScript reference][spec]
    ///  - [MDN documentation][mdn]
    ///
    /// [spec]: https://tc39.es/ecma262/#sec-array.prototype-@@unscopables
    /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/@@unscopables
    pub(crate) fn unscopables_intrinsic(context: &mut Context) -> JsObject {
        // 1. Let unscopableList be OrdinaryObjectCreate(null).
        let unscopable_list = JsObject::empty();
        // 2. Perform ! CreateDataPropertyOrThrow(unscopableList, "at", true).
        unscopable_list
            .create_data_property_or_throw("at", true, context)
            .expect("CreateDataPropertyOrThrow for 'at' must not fail");
        // 3. Perform ! CreateDataPropertyOrThrow(unscopableList, "copyWithin", true).
        unscopable_list
            .create_data_property_or_throw("copyWithin", true, context)
            .expect("CreateDataPropertyOrThrow for 'copyWithin' must not fail");
        // 4. Perform ! CreateDataPropertyOrThrow(unscopableList, "entries", true).
        unscopable_list
            .create_data_property_or_throw("entries", true, context)
            .expect("CreateDataPropertyOrThrow for 'entries' must not fail");
        // 5. Perform ! CreateDataPropertyOrThrow(unscopableList, "fill", true).
        unscopable_list
            .create_data_property_or_throw("fill", true, context)
            .expect("CreateDataPropertyOrThrow for 'fill' must not fail");
        // 6. Perform ! CreateDataPropertyOrThrow(unscopableList, "find", true).
        unscopable_list
            .create_data_property_or_throw("find", true, context)
            .expect("CreateDataPropertyOrThrow for 'find' must not fail");
        // 7. Perform ! CreateDataPropertyOrThrow(unscopableList, "findIndex", true).
        unscopable_list
            .create_data_property_or_throw("findIndex", true, context)
            .expect("CreateDataPropertyOrThrow for 'findIndex' must not fail");
        // 8. Perform ! CreateDataPropertyOrThrow(unscopableList, "flat", true).
        unscopable_list
            .create_data_property_or_throw("flat", true, context)
            .expect("CreateDataPropertyOrThrow for 'flat' must not fail");
        // 9. Perform ! CreateDataPropertyOrThrow(unscopableList, "flatMap", true).
        unscopable_list
            .create_data_property_or_throw("flatMap", true, context)
            .expect("CreateDataPropertyOrThrow for 'flatMap' must not fail");
        // 10. Perform ! CreateDataPropertyOrThrow(unscopableList, "includes", true).
        unscopable_list
            .create_data_property_or_throw("includes", true, context)
            .expect("CreateDataPropertyOrThrow for 'includes' must not fail");
        // 11. Perform ! CreateDataPropertyOrThrow(unscopableList, "keys", true).
        unscopable_list
            .create_data_property_or_throw("keys", true, context)
            .expect("CreateDataPropertyOrThrow for 'keys' must not fail");
        // 12. Perform ! CreateDataPropertyOrThrow(unscopableList, "values", true).
        unscopable_list
            .create_data_property_or_throw("values", true, context)
            .expect("CreateDataPropertyOrThrow for 'values' must not fail");

        // 13. Return unscopableList.
        unscopable_list
    }
}
