/*
 * Copyright (C) 2021 Sony Interactive Entertainment Inc.
 * Copyright (C) 2021 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
 * THE POSSIBILITY OF SUCH DAMAGE.
 */

#include "config.h"
#include "TemporalDuration.h"

#include "IntlObjectInlines.h"
#include "JSCInlines.h"
#include "TemporalObject.h"
#include <wtf/text/StringBuilder.h>
#include <wtf/text/StringConcatenate.h>

namespace JSC {

static constexpr double nanosecondsPerDay = 24.0 * 60 * 60 * 1000 * 1000 * 1000;

const ClassInfo TemporalDuration::s_info = { "Object"_s, &Base::s_info, nullptr, nullptr, CREATE_METHOD_TABLE(TemporalDuration) };

TemporalDuration* TemporalDuration::create(VM& vm, Structure* structure, ISO8601::Duration&& duration)
{
    auto* object = new (NotNull, allocateCell<TemporalDuration>(vm)) TemporalDuration(vm, structure, WTFMove(duration));
    object->finishCreation(vm);
    return object;
}

Structure* TemporalDuration::createStructure(VM& vm, JSGlobalObject* globalObject, JSValue prototype)
{
    return Structure::create(vm, globalObject, prototype, TypeInfo(ObjectType, StructureFlags), info());
}

TemporalDuration::TemporalDuration(VM& vm, Structure* structure, ISO8601::Duration&& duration)
    : Base(vm, structure)
    , m_duration(WTFMove(duration))
{
}

void TemporalDuration::finishCreation(VM& vm)
{
    Base::finishCreation(vm);
    ASSERT(inherits(info()));
}

// CreateTemporalDuration ( years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds [ , newTarget ] )
// https://tc39.es/proposal-temporal/#sec-temporal-createtemporalduration
TemporalDuration* TemporalDuration::tryCreateIfValid(JSGlobalObject* globalObject, ISO8601::Duration&& duration, Structure* structure)
{
    VM& vm = globalObject->vm();
    auto scope = DECLARE_THROW_SCOPE(vm);

    if (!ISO8601::isValidDuration(duration)) {
        throwRangeError(globalObject, scope, "Temporal.Duration properties must be finite and of consistent sign"_s);
        return { };
    }

    return TemporalDuration::create(vm, structure ? structure : globalObject->durationStructure(), WTFMove(duration));
}

// ToTemporalDurationRecord ( temporalDurationLike )
// https://tc39.es/proposal-temporal/#sec-temporal-totemporaldurationrecord
ISO8601::Duration TemporalDuration::fromDurationLike(JSGlobalObject* globalObject, JSObject* durationLike)
{
    VM& vm = globalObject->vm();
    auto scope = DECLARE_THROW_SCOPE(vm);

    if (durationLike->inherits<TemporalDuration>())
        return jsCast<TemporalDuration*>(durationLike)->m_duration;

    ISO8601::Duration result;
    auto hasRelevantProperty = false;
    for (TemporalUnit unit : temporalUnitsInTableOrder) {
        JSValue value = durationLike->get(globalObject, temporalUnitPluralPropertyName(vm, unit));
        RETURN_IF_EXCEPTION(scope, { });

        if (value.isUndefined())
            continue;

        hasRelevantProperty = true;
        result[unit] = value.toIntegerWithoutRounding(globalObject);
        RETURN_IF_EXCEPTION(scope, { });

        if (!isInteger(result[unit])) {
            throwRangeError(globalObject, scope, "Temporal.Duration properties must be integers"_s);
            return { };
        }
    }

    if (!hasRelevantProperty) {
        throwTypeError(globalObject, scope, "Object must contain at least one Temporal.Duration property"_s);
        return { };
    }

    return result;
}

// ToLimitedTemporalDuration ( temporalDurationLike, disallowedFields )
// https://tc39.es/proposal-temporal/#sec-temporal-tolimitedtemporalduration
ISO8601::Duration TemporalDuration::toISO8601Duration(JSGlobalObject* globalObject, JSValue itemValue)
{
    VM& vm = globalObject->vm();
    auto scope = DECLARE_THROW_SCOPE(vm);

    ISO8601::Duration duration;
    if (itemValue.isObject()) {
        duration = fromDurationLike(globalObject, asObject(itemValue));
        RETURN_IF_EXCEPTION(scope, { });
    } else {
        String string = itemValue.toWTFString(globalObject);
        RETURN_IF_EXCEPTION(scope, { });

        auto parsedDuration = ISO8601::parseDuration(string);
        if (!parsedDuration) {
            // 3090: 308 digits * 10 fields + 10 designators
            throwRangeError(globalObject, scope, makeString("'"_s, ellipsizeAt(3090, string), "' is not a valid Duration string"_s));
            return { };
        }

        duration = parsedDuration.value();
    }

    if (!ISO8601::isValidDuration(duration)) {
        throwRangeError(globalObject, scope, "Temporal.Duration properties must be finite and of consistent sign"_s);
        return { };
    }

    return duration;
}

// ToTemporalDuration ( item )
// https://tc39.es/proposal-temporal/#sec-temporal-totemporalduration
TemporalDuration* TemporalDuration::toTemporalDuration(JSGlobalObject* globalObject, JSValue itemValue)
{
    VM& vm = globalObject->vm();
    auto scope = DECLARE_THROW_SCOPE(vm);

    if (itemValue.inherits<TemporalDuration>())
        return jsCast<TemporalDuration*>(itemValue);

    auto result = toISO8601Duration(globalObject, itemValue);
    RETURN_IF_EXCEPTION(scope, nullptr);

    return TemporalDuration::create(vm, globalObject->durationStructure(), WTFMove(result));
}

// ToLimitedTemporalDuration ( temporalDurationLike, disallowedFields )
// https://tc39.es/proposal-temporal/#sec-temporal-tolimitedtemporalduration
ISO8601::Duration TemporalDuration::toLimitedDuration(JSGlobalObject* globalObject, JSValue itemValue, std::initializer_list<TemporalUnit> disallowedUnits)
{
    VM& vm = globalObject->vm();
    auto scope = DECLARE_THROW_SCOPE(vm);

    ISO8601::Duration duration = toISO8601Duration(globalObject, itemValue);
    RETURN_IF_EXCEPTION(scope, { });

    if (!isValidDuration(duration)) {
        throwRangeError(globalObject, scope, "Temporal.Duration properties must be finite and of consistent sign"_s);
        return { };
    }

    for (TemporalUnit unit : disallowedUnits) {
        if (duration[unit]) {
            throwRangeError(globalObject, scope, makeString("Adding "_s, temporalUnitPluralPropertyName(vm, unit).publicName(), " not supported by Temporal.Instant. Try Temporal.ZonedDateTime instead"_s));
            return { };
        }
    }

    return duration;
}

TemporalDuration* TemporalDuration::from(JSGlobalObject* globalObject, JSValue itemValue)
{
    VM& vm = globalObject->vm();

    if (itemValue.inherits<TemporalDuration>()) {
        ISO8601::Duration cloned = jsCast<TemporalDuration*>(itemValue)->m_duration;
        return TemporalDuration::create(vm, globalObject->durationStructure(), WTFMove(cloned));
    }

    return toTemporalDuration(globalObject, itemValue);
}

// TotalDurationNanoseconds ( days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds, offsetShift )
// https://tc39.es/proposal-temporal/#sec-temporal-totaldurationnanoseconds
static double totalNanoseconds(ISO8601::Duration& duration)
{
    auto hours = 24 * duration.days() + duration.hours();
    auto minutes = 60 * hours + duration.minutes();
    auto seconds = 60 * minutes + duration.seconds();
    auto milliseconds = 1000 * seconds + duration.milliseconds();
    auto microseconds = 1000 * milliseconds + duration.microseconds();
    return 1000 * microseconds + duration.nanoseconds();
}

JSValue TemporalDuration::compare(JSGlobalObject* globalObject, JSValue valueOne, JSValue valueTwo)
{
    VM& vm = globalObject->vm();
    auto scope = DECLARE_THROW_SCOPE(vm);

    auto* one = toTemporalDuration(globalObject, valueOne);
    RETURN_IF_EXCEPTION(scope, { });

    auto* two = toTemporalDuration(globalObject, valueTwo);
    RETURN_IF_EXCEPTION(scope, { });

    // FIXME: Implement relativeTo parameter after PlainDateTime / ZonedDateTime.
    if (one->years() || two->years() || one->months() || two->months() || one->weeks() || two->weeks()) {
        throwRangeError(globalObject, scope, "Cannot compare a duration of years, months, or weeks without a relativeTo option"_s);
        return { };
    }

    auto nsOne = totalNanoseconds(one->m_duration);
    auto nsTwo = totalNanoseconds(two->m_duration);
    return jsNumber(nsOne > nsTwo ? 1 : nsOne < nsTwo ? -1 : 0);
}

int TemporalDuration::sign(const ISO8601::Duration& duration)
{
    for (auto value : duration) {
        if (value < 0)
            return -1;

        if (value > 0)
            return 1;
    }

    return 0;
}

ISO8601::Duration TemporalDuration::with(JSGlobalObject* globalObject, JSObject* durationLike) const
{
    VM& vm = globalObject->vm();
    auto scope = DECLARE_THROW_SCOPE(vm);

    ISO8601::Duration result;
    auto hasRelevantProperty = false;
    for (TemporalUnit unit : temporalUnitsInTableOrder) {
        JSValue value = durationLike->get(globalObject, temporalUnitPluralPropertyName(vm, unit));
        RETURN_IF_EXCEPTION(scope, { });

        if (value.isUndefined()) {
            result[unit] = m_duration[unit];
            continue;
        }

        hasRelevantProperty = true;
        result[unit] = value.toNumber(globalObject);
        RETURN_IF_EXCEPTION(scope, { });

        if (!isInteger(result[unit])) {
            throwRangeError(globalObject, scope, "Temporal.Duration properties must be integers"_s);
            return { };
        }
    }

    if (!hasRelevantProperty) {
        throwTypeError(globalObject, scope, "Object must contain at least one Temporal.Duration property"_s);
        return { };
    }

    return result;
}

ISO8601::Duration TemporalDuration::negated() const
{
    return -m_duration;
}

ISO8601::Duration TemporalDuration::abs() const
{
    ISO8601::Duration result;
    for (size_t i = 0; i < numberOfTemporalUnits; i++)
        result[i] = std::abs(m_duration[i]);
    return result;
}

// DefaultTemporalLargestUnit ( years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds )
// https://tc39.es/proposal-temporal/#sec-temporal-defaulttemporallargestunit
static TemporalUnit largestSubduration(const ISO8601::Duration& duration)
{
    uint8_t index = 0;
    while (index < numberOfTemporalUnits - 1 && !duration[index])
        index++;
    return static_cast<TemporalUnit>(index);
}

// BalanceDuration ( days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds, largestUnit [ , relativeTo ] )
// https://tc39.es/proposal-temporal/#sec-temporal-balanceduration
void TemporalDuration::balance(ISO8601::Duration& duration, TemporalUnit largestUnit)
{
    auto nanoseconds = totalNanoseconds(duration);
    duration.clear();

    if (largestUnit <= TemporalUnit::Day) {
        duration.setDays(std::trunc(nanoseconds / nanosecondsPerDay));
        nanoseconds = std::fmod(nanoseconds, nanosecondsPerDay);
    }

    double microseconds = std::trunc(nanoseconds / 1000);
    double milliseconds = std::trunc(microseconds / 1000);
    double seconds = std::trunc(milliseconds / 1000);
    double minutes = std::trunc(seconds / 60);
    if (largestUnit <= TemporalUnit::Hour) {
        duration.setNanoseconds(std::fmod(nanoseconds, 1000));
        duration.setMicroseconds(std::fmod(microseconds, 1000));
        duration.setMilliseconds(std::fmod(milliseconds, 1000));
        duration.setSeconds(std::fmod(seconds, 60));
        duration.setMinutes(std::fmod(minutes, 60));
        duration.setHours(std::trunc(minutes / 60));
    } else if (largestUnit == TemporalUnit::Minute) {
        duration.setNanoseconds(std::fmod(nanoseconds, 1000));
        duration.setMicroseconds(std::fmod(microseconds, 1000));
        duration.setMilliseconds(std::fmod(milliseconds, 1000));
        duration.setSeconds(std::fmod(seconds, 60));
        duration.setMinutes(minutes);
    } else if (largestUnit == TemporalUnit::Second) {
        duration.setNanoseconds(std::fmod(nanoseconds, 1000));
        duration.setMicroseconds(std::fmod(microseconds, 1000));
        duration.setMilliseconds(std::fmod(milliseconds, 1000));
        duration.setSeconds(seconds);
    } else if (largestUnit == TemporalUnit::Millisecond) {
        duration.setNanoseconds(std::fmod(nanoseconds, 1000));
        duration.setMicroseconds(std::fmod(microseconds, 1000));
        duration.setMilliseconds(milliseconds);
    } else if (largestUnit == TemporalUnit::Microsecond) {
        duration.setNanoseconds(std::fmod(nanoseconds, 1000));
        duration.setMicroseconds(microseconds);
    } else
        duration.setNanoseconds(nanoseconds);
}

ISO8601::Duration TemporalDuration::add(JSGlobalObject* globalObject, JSValue otherValue) const
{
    VM& vm = globalObject->vm();
    auto scope = DECLARE_THROW_SCOPE(vm);

    auto other = toISO8601Duration(globalObject, otherValue);
    RETURN_IF_EXCEPTION(scope, { });

    // FIXME: Implement relativeTo parameter after PlainDateTime / ZonedDateTime.
    auto largestUnit = std::min(largestSubduration(m_duration), largestSubduration(other));
    if (largestUnit <= TemporalUnit::Week) {
        throwRangeError(globalObject, scope, "Cannot add a duration of years, months, or weeks without a relativeTo option"_s);
        return { };
    }

    ISO8601::Duration result {
        0, 0, 0, days() + other.days(),
        hours() + other.hours(), minutes() + other.minutes(), seconds() + other.seconds(),
        milliseconds() + other.milliseconds(), microseconds() + other.microseconds(), nanoseconds() + other.nanoseconds()
    };

    balance(result, largestUnit);
    return result;
}

ISO8601::Duration TemporalDuration::subtract(JSGlobalObject* globalObject, JSValue otherValue) const
{
    VM& vm = globalObject->vm();
    auto scope = DECLARE_THROW_SCOPE(vm);

    auto other = toISO8601Duration(globalObject, otherValue);
    RETURN_IF_EXCEPTION(scope, { });

    // FIXME: Implement relativeTo parameter after PlainDateTime / ZonedDateTime.
    auto largestUnit = std::min(largestSubduration(m_duration), largestSubduration(other));
    if (largestUnit <= TemporalUnit::Week) {
        throwRangeError(globalObject, scope, "Cannot subtract a duration of years, months, or weeks without a relativeTo option"_s);
        return { };
    }

    ISO8601::Duration result {
        0, 0, 0, days() - other.days(),
        hours() - other.hours(), minutes() - other.minutes(), seconds() - other.seconds(),
        milliseconds() - other.milliseconds(), microseconds() - other.microseconds(), nanoseconds() - other.nanoseconds()
    };

    balance(result, largestUnit);
    return result;
}

// RoundDuration ( years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds, increment, unit, roundingMode [ , relativeTo ] )
// https://tc39.es/proposal-temporal/#sec-temporal-roundduration
double TemporalDuration::round(ISO8601::Duration& duration, double increment, TemporalUnit unit, RoundingMode mode)
{
    ASSERT(unit >= TemporalUnit::Day);
    double remainder = 0;

    if (unit == TemporalUnit::Day) {
        auto originalDays = duration.days();
        duration.setDays(0);
        auto nanoseconds = totalNanoseconds(duration);

        auto fractionalDays = originalDays + nanoseconds / nanosecondsPerDay;
        auto newDays = roundNumberToIncrement(fractionalDays, increment, mode);
        remainder = fractionalDays - newDays;
        duration.setDays(newDays);
    } else if (unit == TemporalUnit::Hour) {
        auto fractionalSeconds = duration.seconds() + duration.milliseconds() * 1e-3 + duration.microseconds() * 1e-6 + duration.nanoseconds() * 1e-9;
        auto fractionalHours = duration.hours() + (duration.minutes() + fractionalSeconds / 60) / 60;
        auto newHours = roundNumberToIncrement(fractionalHours, increment, mode);
        remainder = fractionalHours - newHours;
        duration.setHours(newHours);
    } else if (unit == TemporalUnit::Minute) {
        auto fractionalSeconds = duration.seconds() + duration.milliseconds() * 1e-3 + duration.microseconds() * 1e-6 + duration.nanoseconds() * 1e-9;
        auto fractionalMinutes = duration.minutes() + fractionalSeconds / 60;
        auto newMinutes = roundNumberToIncrement(fractionalMinutes, increment, mode);
        remainder = fractionalMinutes - newMinutes;
        duration.setMinutes(newMinutes);
    } else if (unit == TemporalUnit::Second) {
        auto fractionalSeconds = duration.seconds() + duration.milliseconds() * 1e-3 + duration.microseconds() * 1e-6 + duration.nanoseconds() * 1e-9;
        auto newSeconds = roundNumberToIncrement(fractionalSeconds, increment, mode);
        remainder = fractionalSeconds - newSeconds;
        duration.setSeconds(newSeconds);
    } else if (unit == TemporalUnit::Millisecond) {
        auto fractionalMilliseconds = duration.milliseconds() + duration.microseconds() * 1e-3 + duration.nanoseconds() * 1e-6;
        auto newMilliseconds = roundNumberToIncrement(fractionalMilliseconds, increment, mode);
        remainder = fractionalMilliseconds - newMilliseconds;
        duration.setMilliseconds(newMilliseconds);
    } else if (unit == TemporalUnit::Microsecond) {
        auto fractionalMicroseconds = duration.microseconds() + duration.nanoseconds() * 1e-3;
        auto newMicroseconds = roundNumberToIncrement(fractionalMicroseconds, increment, mode);
        remainder = fractionalMicroseconds - newMicroseconds;
        duration.setMicroseconds(newMicroseconds);
    } else {
        auto newNanoseconds = roundNumberToIncrement(duration.nanoseconds(), increment, mode);
        remainder = duration.nanoseconds() - newNanoseconds;
        duration.setNanoseconds(newNanoseconds);
    }

    for (auto i = static_cast<uint8_t>(unit) + 1u; i < numberOfTemporalUnits; i++)
        duration[i] = 0;

    return remainder;
}

ISO8601::Duration TemporalDuration::round(JSGlobalObject* globalObject, JSValue optionsValue) const
{
    VM& vm = globalObject->vm();
    auto scope = DECLARE_THROW_SCOPE(vm);

    JSObject* options = nullptr;
    std::optional<TemporalUnit> smallest;
    std::optional<TemporalUnit> largest;
    TemporalUnit defaultLargestUnit = largestSubduration(m_duration);
    if (optionsValue.isString()) {
        auto string = optionsValue.toWTFString(globalObject);
        RETURN_IF_EXCEPTION(scope, { });

        smallest = temporalUnitType(string);
        if (!smallest) {
            throwRangeError(globalObject, scope, "smallestUnit is an invalid Temporal unit"_s);
            return { };
        }
    } else {
        options = intlGetOptionsObject(globalObject, optionsValue);
        RETURN_IF_EXCEPTION(scope, { });

        smallest = temporalSmallestUnit(globalObject, options, { });
        RETURN_IF_EXCEPTION(scope, { });

        largest = temporalLargestUnit(globalObject, options, { }, defaultLargestUnit);
        RETURN_IF_EXCEPTION(scope, { });

        if (!smallest && !largest) {
            throwRangeError(globalObject, scope, "Cannot round without a smallestUnit or largestUnit option"_s);
            return { };
        }

        if (smallest && largest && smallest.value() < largest.value()) {
            throwRangeError(globalObject, scope, "smallestUnit must be smaller than largestUnit"_s);
            return { };
        }
    }
    TemporalUnit smallestUnit = smallest.value_or(TemporalUnit::Nanosecond);
    TemporalUnit largestUnit = largest.value_or(std::min(defaultLargestUnit, smallestUnit));

    auto roundingMode = temporalRoundingMode(globalObject, options, RoundingMode::HalfExpand);
    RETURN_IF_EXCEPTION(scope, { });

    auto increment = temporalRoundingIncrement(globalObject, options, maximumRoundingIncrement(smallestUnit), false);
    RETURN_IF_EXCEPTION(scope, { });

    // FIXME: Implement relativeTo parameter after PlainDateTime / ZonedDateTime.
    if (largestUnit > TemporalUnit::Year && (years() || months() || weeks() || (days() && largestUnit < TemporalUnit::Day))) {
        throwRangeError(globalObject, scope, "Cannot round a duration of years, months, or weeks without a relativeTo option"_s);
        return { };
    }
    if (largestUnit <= TemporalUnit::Week) {
        throwVMError(globalObject, scope, "FIXME: years, months, or weeks rounding with relativeTo not implemented yet"_s);
        return { };
    }

    ISO8601::Duration newDuration = m_duration;
    round(newDuration, increment, smallestUnit, roundingMode);
    balance(newDuration, largestUnit);
    return newDuration;
}

double TemporalDuration::total(JSGlobalObject* globalObject, JSValue optionsValue) const
{
    VM& vm = globalObject->vm();
    auto scope = DECLARE_THROW_SCOPE(vm);

    String unitString;
    if (optionsValue.isString()) {
        unitString = optionsValue.toWTFString(globalObject);
        RETURN_IF_EXCEPTION(scope, 0);
    } else {
        JSObject* options = intlGetOptionsObject(globalObject, optionsValue);
        RETURN_IF_EXCEPTION(scope, 0);

        unitString = intlStringOption(globalObject, options, vm.propertyNames->unit, { }, { }, { });
        RETURN_IF_EXCEPTION(scope, 0);
    }

    auto unitType = temporalUnitType(unitString);
    if (!unitType) {
        throwRangeError(globalObject, scope, "unit is an invalid Temporal unit"_s);
        return 0;
    }
    TemporalUnit unit = unitType.value();

    // FIXME: Implement relativeTo parameter after PlainDateTime / ZonedDateTime.
    if (unit > TemporalUnit::Year && (years() || months() || weeks() || (days() && unit < TemporalUnit::Day))) {
        throwRangeError(globalObject, scope, "Cannot total a duration of years, months, or weeks without a relativeTo option"_s);
        return { };
    }

    ISO8601::Duration newDuration = m_duration;
    balance(newDuration, unit);
    double remainder = round(newDuration, 1, unit, RoundingMode::Trunc);
    return newDuration[static_cast<uint8_t>(unit)] + remainder;
}

String TemporalDuration::toString(JSGlobalObject* globalObject, JSValue optionsValue) const
{
    VM& vm = globalObject->vm();
    auto scope = DECLARE_THROW_SCOPE(vm);

    JSObject* options = intlGetOptionsObject(globalObject, optionsValue);
    RETURN_IF_EXCEPTION(scope, { });

    if (!options)
        return toString();

    PrecisionData data = secondsStringPrecision(globalObject, options);
    RETURN_IF_EXCEPTION(scope, { });
    if (data.unit < TemporalUnit::Second) {
        throwRangeError(globalObject, scope, "smallestUnit must not be \"minute\""_s);
        return { };
    }

    auto roundingMode = temporalRoundingMode(globalObject, options, RoundingMode::Trunc);
    RETURN_IF_EXCEPTION(scope, { });

    // No need to make a new object if we were given explicit defaults.
    if (std::get<0>(data.precision) == Precision::Auto && roundingMode == RoundingMode::Trunc)
        return toString();

    ISO8601::Duration newDuration = m_duration;
    round(newDuration, data.increment, data.unit, roundingMode);
    return toString(newDuration, data.precision);
}

// TemporalDurationToString ( years, months, weeks, days, hours, minutes, seconds, milliseconds, microseconds, nanoseconds, precision )
// https://tc39.es/proposal-temporal/#sec-temporal-temporaldurationtostring
String TemporalDuration::toString(const ISO8601::Duration& duration, std::tuple<Precision, unsigned> precision)
{
    ASSERT(std::get<0>(precision) == Precision::Auto || std::get<1>(precision) < 10);

    auto balancedMicroseconds = duration.microseconds() + std::trunc(duration.nanoseconds() / 1000);
    auto balancedNanoseconds = std::fmod(duration.nanoseconds(), 1000);
    auto balancedMilliseconds = duration.milliseconds() + std::trunc(balancedMicroseconds / 1000);
    balancedMicroseconds = std::fmod(balancedMicroseconds, 1000);
    auto balancedSeconds = duration.seconds() + std::trunc(balancedMilliseconds / 1000);
    balancedMilliseconds = std::fmod(balancedMilliseconds, 1000);

    // TEMPORARY! (pending spec discussion about maximum values @ https://github.com/tc39/proposal-temporal/issues/1604)
    // We *must* avoid printing a number in scientific notation, which is currently only possible for numbers < 1e21
    // (a value originating in the Number#toFixed spec and upheld by our NumberToStringBuffer).
    auto formatInteger = [](double value) -> double {
        auto absValue = std::abs(value);
        return LIKELY(absValue < 1e21) ? absValue : 1e21 - 65537;
    };

    StringBuilder builder;

    auto sign = TemporalDuration::sign(duration);
    if (sign < 0)
        builder.append('-');

    builder.append('P');
    if (duration.years())
        builder.append(formatInteger(duration.years()), 'Y');
    if (duration.months())
        builder.append(formatInteger(duration.months()), 'M');
    if (duration.weeks())
        builder.append(formatInteger(duration.weeks()), 'W');
    if (duration.days())
        builder.append(formatInteger(duration.days()), 'D');

    // The zero value is displayed in seconds.
    auto usesSeconds = balancedSeconds || balancedMilliseconds || balancedMicroseconds || balancedNanoseconds || !sign || std::get<0>(precision) != Precision::Auto;
    if (!duration.hours() && !duration.minutes() && !usesSeconds)
        return builder.toString();

    builder.append('T');
    if (duration.hours())
        builder.append(formatInteger(duration.hours()), 'H');
    if (duration.minutes())
        builder.append(formatInteger(duration.minutes()), 'M');
    if (usesSeconds) {
        builder.append(formatInteger(balancedSeconds));

        auto fraction = std::abs(balancedMilliseconds) * 1e6 + std::abs(balancedMicroseconds) * 1e3 + std::abs(balancedNanoseconds);
        formatSecondsStringFraction(builder, fraction, precision);

        builder.append('S');
    }

    return builder.toString();
}

} // namespace JSC
