/*
 * Copyright (C) 2021 Apple Inc.
 *
 * 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 "TemporalPlainTime.h"

#include "AbstractSlotVisitor.h"
#include "IntlObjectInlines.h"
#include "JSCInlines.h"
#include "LazyPropertyInlines.h"
#include "TemporalDuration.h"
#include "VMTrapsInlines.h"

namespace JSC {
namespace TemporalPlainTimeInternal {
static constexpr bool verbose = false;
}

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

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

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

TemporalPlainTime::TemporalPlainTime(VM& vm, Structure* structure, ISO8601::PlainTime&& plainTime)
    : Base(vm, structure)
    , m_plainTime(WTFMove(plainTime))
{
}

void TemporalPlainTime::finishCreation(VM& vm)
{
    Base::finishCreation(vm);
    ASSERT(inherits(info()));
    m_calendar.initLater(
        [] (const auto& init) {
            VM& vm = init.vm;
            auto* plainTime = jsCast<TemporalPlainTime*>(init.owner);
            auto* globalObject = plainTime->globalObject();
            auto* calendar = TemporalCalendar::create(vm, globalObject->calendarStructure(), iso8601CalendarID());
            init.set(calendar);
        });
}

template<typename Visitor>
void TemporalPlainTime::visitChildrenImpl(JSCell* cell, Visitor& visitor)
{
    Base::visitChildren(cell, visitor);

    auto* thisObject = jsCast<TemporalPlainTime*>(cell);
    thisObject->m_calendar.visit(visitor);
}

DEFINE_VISIT_CHILDREN(TemporalPlainTime);

// https://tc39.es/proposal-temporal/#sec-temporal-isvalidtime
static ISO8601::PlainTime toPlainTime(JSGlobalObject* globalObject, ISO8601::Duration&& duration)
{
    VM& vm = globalObject->vm();
    auto scope = DECLARE_THROW_SCOPE(vm);

    double hour = duration.hours();
    double minute = duration.minutes();
    double second = duration.seconds();
    double millisecond = duration.milliseconds();
    double microsecond = duration.microseconds();
    double nanosecond = duration.nanoseconds();
    if (!(hour >= 0 && hour <= 23)) {
        throwRangeError(globalObject, scope, "hour is out of range"_s);
        return { };
    }
    if (!(minute >= 0 && minute <= 59)) {
        throwRangeError(globalObject, scope, "minute is out of range"_s);
        return { };
    }
    if (!(second >= 0 && second <= 59)) {
        throwRangeError(globalObject, scope, "second is out of range"_s);
        return { };
    }
    if (!(millisecond >= 0 && millisecond <= 999)) {
        throwRangeError(globalObject, scope, "millisecond is out of range"_s);
        return { };
    }
    if (!(microsecond >= 0 && microsecond <= 999)) {
        throwRangeError(globalObject, scope, "microsecond is out of range"_s);
        return { };
    }
    if (!(nanosecond >= 0 && nanosecond <= 999)) {
        throwRangeError(globalObject, scope, "nanosecond is out of range"_s);
        return { };
    }
    return ISO8601::PlainTime {
        static_cast<unsigned>(hour),
        static_cast<unsigned>(minute),
        static_cast<unsigned>(second),
        static_cast<unsigned>(millisecond),
        static_cast<unsigned>(microsecond),
        static_cast<unsigned>(nanosecond)
    };
}

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

    auto plainTime = toPlainTime(globalObject, WTFMove(duration));
    RETURN_IF_EXCEPTION(scope, { });

    return TemporalPlainTime::create(vm, structure, WTFMove(plainTime));
}

static double nonNegativeModulo(double x, double y)
{
    double result = std::fmod(x, y);
    if (!result)
        return 0;
    if (result < 0)
        result += y;
    return result;
}

// https://tc39.es/proposal-temporal/#sec-temporal-balancetime
static ISO8601::Duration balanceTime(double hour, double minute, double second, double millisecond, double microsecond, double nanosecond)
{
    // https://github.com/tc39/proposal-temporal/issues/1804
    // Use non-negative modulo operation.
    microsecond += std::floor(nanosecond / 1000);
    nanosecond = nonNegativeModulo(nanosecond, 1000);
    millisecond += std::floor(microsecond / 1000);
    microsecond = nonNegativeModulo(microsecond, 1000);
    second += std::floor(millisecond / 1000);
    millisecond = nonNegativeModulo(millisecond, 1000);
    minute += std::floor(second / 60);
    second = nonNegativeModulo(second, 60);
    hour += std::floor(minute / 60);
    minute = nonNegativeModulo(minute, 60);
    double days = std::floor(hour / 24);
    hour = nonNegativeModulo(hour, 24);
    return ISO8601::Duration(0, 0, 0, days, hour, minute, second, millisecond, microsecond, nanosecond);
}

// https://tc39.es/proposal-temporal/#sec-temporal-roundtime
static ISO8601::Duration roundTime(ISO8601::PlainTime plainTime, double increment, TemporalUnit unit, RoundingMode roundingMode, std::optional<double> dayLengthNs)
{
    auto fractionalSecond = [](ISO8601::PlainTime plainTime) -> double {
        return plainTime.second() + plainTime.millisecond() * 1e-3 + plainTime.microsecond() * 1e-6 + plainTime.nanosecond() * 1e-9;
    };

    double quantity = 0;
    switch (unit) {
    case TemporalUnit::Day: {
        double length = dayLengthNs.value_or(8.64 * 1e+13);
        quantity = ((((plainTime.hour() * 60.0 + plainTime.minute()) * 60.0 + plainTime.second()) * 1000.0 + plainTime.millisecond()) * 1000.0 + plainTime.nanosecond()) / length;
        auto result = roundNumberToIncrement(quantity, increment, roundingMode);
        return ISO8601::Duration(0, 0, 0, result, 0, 0, 0, 0, 0, 0);
    }
    case TemporalUnit::Hour: {
        quantity = (fractionalSecond(plainTime) / 60.0 + plainTime.minute()) / 60.0 + plainTime.hour();
        auto result = roundNumberToIncrement(quantity, increment, roundingMode);
        return balanceTime(result, 0, 0, 0, 0, 0);
    }
    case TemporalUnit::Minute: {
        quantity = fractionalSecond(plainTime) / 60.0 + plainTime.minute();
        auto result = roundNumberToIncrement(quantity, increment, roundingMode);
        return balanceTime(plainTime.hour(), result, 0, 0, 0, 0);
    }
    case TemporalUnit::Second: {
        quantity = fractionalSecond(plainTime);
        auto result = roundNumberToIncrement(quantity, increment, roundingMode);
        return balanceTime(plainTime.hour(), plainTime.minute(), result, 0, 0, 0);
    }
    case TemporalUnit::Millisecond: {
        quantity = plainTime.millisecond() + plainTime.microsecond() * 1e-3 + plainTime.nanosecond() * 1e-6;
        auto result = roundNumberToIncrement(quantity, increment, roundingMode);
        return balanceTime(plainTime.hour(), plainTime.minute(), plainTime.second(), result, 0, 0);
    }
    case TemporalUnit::Microsecond: {
        quantity = plainTime.microsecond() + plainTime.nanosecond() * 1e-3;
        auto result = roundNumberToIncrement(quantity, increment, roundingMode);
        return balanceTime(plainTime.hour(), plainTime.minute(), plainTime.second(), plainTime.millisecond(), result, 0);
    }
    case TemporalUnit::Nanosecond: {
        quantity = plainTime.nanosecond();
        auto result = roundNumberToIncrement(quantity, increment, roundingMode);
        return balanceTime(plainTime.hour(), plainTime.minute(), plainTime.second(), plainTime.millisecond(), plainTime.microsecond(), result);
    }
    default:
        RELEASE_ASSERT_NOT_REACHED();
        break;
    }

    return ISO8601::Duration();
}

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

    JSObject* options = nullptr;
    std::optional<TemporalUnit> smallest;
    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 { };
        }

        if (smallest.value() <= TemporalUnit::Day) {
            throwRangeError(globalObject, scope, "smallestUnit is a disallowed unit"_s);
            return { };
        }
    } else {
        options = intlGetOptionsObject(globalObject, optionsValue);
        RETURN_IF_EXCEPTION(scope, { });

        smallest = temporalSmallestUnit(globalObject, options, { TemporalUnit::Year, TemporalUnit::Month, TemporalUnit::Week, TemporalUnit::Day });
        RETURN_IF_EXCEPTION(scope, { });
        if (!smallest) {
            throwRangeError(globalObject, scope, "Cannot round without a smallestUnit option"_s);
            return { };
        }
    }
    TemporalUnit smallestUnit = smallest.value();

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

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

    auto duration = roundTime(m_plainTime, increment, smallestUnit, roundingMode, std::nullopt);
    RELEASE_AND_RETURN(scope, toPlainTime(globalObject, WTFMove(duration)));
}

String TemporalPlainTime::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, { });

    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();

    auto duration = roundTime(m_plainTime, data.increment, data.unit, roundingMode, std::nullopt);
    auto plainTime = toPlainTime(globalObject, WTFMove(duration));
    RETURN_IF_EXCEPTION(scope, { });

    return ISO8601::temporalTimeToString(plainTime, data.precision);
}

static ISO8601::Duration toTemporalTimeRecord(JSGlobalObject* globalObject, JSObject* temporalTimeLike)
{
    VM& vm = globalObject->vm();
    auto scope = DECLARE_THROW_SCOPE(vm);

    ISO8601::Duration duration { };
    auto hasRelevantProperty = false;
    for (TemporalUnit unit : temporalUnitsInTableOrder) {
        if (unit < TemporalUnit::Hour)
            continue;
        auto name = temporalUnitSingularPropertyName(vm, unit);
        JSValue value = temporalTimeLike->get(globalObject, name);
        RETURN_IF_EXCEPTION(scope, { });

        if (value.isUndefined())
            continue;

        hasRelevantProperty = true;
        double integer = value.toIntegerOrInfinity(globalObject);
        RETURN_IF_EXCEPTION(scope, { });
        if (!std::isfinite(integer)) {
            throwRangeError(globalObject, scope, "Temporal.PlainTime properties must be finite"_s);
            return { };
        }
        duration[unit] = integer;
    }

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

    return duration;
}

static std::array<std::optional<double>, numberOfTemporalPlainTimeUnits> toPartialTime(JSGlobalObject* globalObject, JSObject* temporalTimeLike)
{
    VM& vm = globalObject->vm();
    auto scope = DECLARE_THROW_SCOPE(vm);

    bool hasAnyFields = false;
    std::array<std::optional<double>, numberOfTemporalPlainTimeUnits> partialTime { };
    for (TemporalUnit unit : temporalUnitsInTableOrder) {
        if (unit < TemporalUnit::Hour)
            continue;
        auto name = temporalUnitSingularPropertyName(vm, unit);
        JSValue value = temporalTimeLike->get(globalObject, name);
        RETURN_IF_EXCEPTION(scope, { });

        if (!value.isUndefined()) {
            hasAnyFields = true;
            double doubleValue = value.toIntegerOrInfinity(globalObject);
            RETURN_IF_EXCEPTION(scope, { });
            if (!std::isfinite(doubleValue)) {
                throwRangeError(globalObject, scope, "toPartialTime properties must be finite"_s);
                return { };
            }
            partialTime[static_cast<unsigned>(unit) - static_cast<unsigned>(TemporalUnit::Hour)] = doubleValue;
        }
    }
    if (!hasAnyFields) {
        throwTypeError(globalObject, scope, "toPartialTime requires at least one property"_s);
        return { };
    }
    return partialTime;
}

static ISO8601::PlainTime constrainTime(ISO8601::Duration&& duration)
{
    auto constrainToRange = [](double value, unsigned minimum, unsigned maximum) -> unsigned {
        if (std::isnan(value))
            return 0;
        return static_cast<unsigned>(std::min<double>(std::max<double>(value, minimum), maximum));
    };
    return ISO8601::PlainTime(
        constrainToRange(duration.hours(), 0, 23),
        constrainToRange(duration.minutes(), 0, 59),
        constrainToRange(duration.seconds(), 0, 59),
        constrainToRange(duration.milliseconds(), 0, 999),
        constrainToRange(duration.microseconds(), 0, 999),
        constrainToRange(duration.nanoseconds(), 0, 999));
}

static ISO8601::PlainTime regulateTime(JSGlobalObject* globalObject, ISO8601::Duration&& duration, TemporalOverflow overflow)
{
    switch (overflow) {
    case TemporalOverflow::Constrain:
        return constrainTime(WTFMove(duration));
    case TemporalOverflow::Reject:
        return toPlainTime(globalObject, WTFMove(duration));
    }
    return { };
}

static JSObject* toTemporalCalendarWithISODefault(JSGlobalObject* globalObject, JSValue temporalCalendarLike)
{
    if (temporalCalendarLike.isUndefined())
        return TemporalCalendar::create(globalObject->vm(), globalObject->calendarStructure(), iso8601CalendarID());
    return TemporalCalendar::from(globalObject, temporalCalendarLike);
}

static JSObject* getTemporalCalendarWithISODefault(JSGlobalObject* globalObject, JSValue itemValue)
{
    VM& vm = globalObject->vm();
    auto scope = DECLARE_THROW_SCOPE(vm);

    if (itemValue.inherits<TemporalPlainTime>())
        return jsCast<TemporalPlainTime*>(itemValue)->calendar();

    JSValue calendar = itemValue.get(globalObject, vm.propertyNames->calendar);
    RETURN_IF_EXCEPTION(scope, { });
    RELEASE_AND_RETURN(scope, toTemporalCalendarWithISODefault(globalObject, calendar));
}

// https://tc39.es/proposal-temporal/#sec-temporal-totemporaltime
TemporalPlainTime* TemporalPlainTime::from(JSGlobalObject* globalObject, JSValue itemValue, std::optional<TemporalOverflow> overflowValue)
{
    VM& vm = globalObject->vm();
    auto scope = DECLARE_THROW_SCOPE(vm);

    auto overflow = overflowValue.value_or(TemporalOverflow::Constrain);

    if (itemValue.isObject()) {
        if (itemValue.inherits<TemporalPlainTime>())
            return jsCast<TemporalPlainTime*>(itemValue);
        JSObject* calendar = getTemporalCalendarWithISODefault(globalObject, itemValue);
        RETURN_IF_EXCEPTION(scope, { });
        JSString* calendarString = calendar->toString(globalObject);
        RETURN_IF_EXCEPTION(scope, { });
        String calendarWTFString = calendarString->value(globalObject);
        RETURN_IF_EXCEPTION(scope, { });
        if (calendarWTFString != "iso8601"_s) {
            throwRangeError(globalObject, scope, "calendar is not iso8601"_s);
            return { };
        }
        auto duration = toTemporalTimeRecord(globalObject, jsCast<JSObject*>(itemValue));
        RETURN_IF_EXCEPTION(scope, { });
        auto plainTime = regulateTime(globalObject, WTFMove(duration), overflow);
        RETURN_IF_EXCEPTION(scope, { });
        return TemporalPlainTime::create(vm, globalObject->plainTimeStructure(), WTFMove(plainTime));
    }

    // https://tc39.es/proposal-temporal/#sec-temporal-parsetemporaltimestring
    // TemporalTimeString :
    //    CalendarTime
    //    CalendarDateTimeTimeRequired

    auto string = itemValue.toWTFString(globalObject);
    RETURN_IF_EXCEPTION(scope, { });

    auto time = ISO8601::parseCalendarTime(string);
    if (time) {
        auto [plainTime, timeZoneOptional, calendarOptional] = WTFMove(time.value());
        if (!(timeZoneOptional && timeZoneOptional->m_z))
            return TemporalPlainTime::create(vm, globalObject->plainTimeStructure(), WTFMove(plainTime));
    }

    auto dateTime = ISO8601::parseCalendarDateTime(string);
    if (dateTime) {
        auto [plainDate, plainTimeOptional, timeZoneOptional, calendarOptional] = WTFMove(dateTime.value());
        if (plainTimeOptional) {
            if (!(timeZoneOptional && timeZoneOptional->m_z))
                return TemporalPlainTime::create(vm, globalObject->plainTimeStructure(), WTFMove(plainTimeOptional.value()));
        }
    }

    throwRangeError(globalObject, scope, "invalid time string"_s);
    return { };
}

// https://tc39.es/proposal-temporal/#sec-temporal-comparetemporaltime
int32_t TemporalPlainTime::compare(TemporalPlainTime* plainTime1, TemporalPlainTime* plainTime2)
{
    ISO8601::PlainTime t1 = plainTime1->plainTime();
    ISO8601::PlainTime t2 = plainTime2->plainTime();
    if (t1.hour() > t2.hour())
        return 1;
    if (t1.hour() < t2.hour())
        return -1;
    if (t1.minute() > t2.minute())
        return 1;
    if (t1.minute() < t2.minute())
        return -1;
    if (t1.second() > t2.second())
        return 1;
    if (t1.second() < t2.second())
        return -1;
    if (t1.millisecond() > t2.millisecond())
        return 1;
    if (t1.millisecond() < t2.millisecond())
        return -1;
    if (t1.microsecond() > t2.microsecond())
        return 1;
    if (t1.microsecond() < t2.microsecond())
        return -1;
    if (t1.nanosecond() > t2.nanosecond())
        return 1;
    if (t1.nanosecond() < t2.nanosecond())
        return -1;
    return 0;
}

static ISO8601::Duration addTime(const ISO8601::PlainTime& plainTime, const ISO8601::Duration& duration)
{
    return balanceTime(
        plainTime.hour() + duration.hours(),
        plainTime.minute() + duration.minutes(),
        plainTime.second() + duration.seconds(),
        plainTime.millisecond() + duration.milliseconds(),
        plainTime.microsecond() + duration.microseconds(),
        plainTime.nanosecond() + duration.nanoseconds());
}

ISO8601::PlainTime TemporalPlainTime::add(JSGlobalObject* globalObject, JSValue temporalDurationLike) const
{
    VM& vm = globalObject->vm();
    auto scope = DECLARE_THROW_SCOPE(vm);

    auto duration = TemporalDuration::toISO8601Duration(globalObject, temporalDurationLike);
    RETURN_IF_EXCEPTION(scope, { });

    RELEASE_AND_RETURN(scope, toPlainTime(globalObject, addTime(m_plainTime, duration)));
}

ISO8601::PlainTime TemporalPlainTime::subtract(JSGlobalObject* globalObject, JSValue temporalDurationLike) const
{
    VM& vm = globalObject->vm();
    auto scope = DECLARE_THROW_SCOPE(vm);

    auto duration = TemporalDuration::toISO8601Duration(globalObject, temporalDurationLike);
    RETURN_IF_EXCEPTION(scope, { });

    RELEASE_AND_RETURN(scope, toPlainTime(globalObject, addTime(m_plainTime, -duration)));
}

ISO8601::PlainTime TemporalPlainTime::with(JSGlobalObject* globalObject, JSObject* temporalTimeLike, JSValue optionsValue) const
{
    VM& vm = globalObject->vm();
    auto scope = DECLARE_THROW_SCOPE(vm);

    if (temporalTimeLike->inherits<TemporalPlainTime>()) {
        throwTypeError(globalObject, scope, "argument object must not carry calendar"_s);
        return { };
    }

    JSValue calendarProperty = temporalTimeLike->get(globalObject, vm.propertyNames->calendar);
    RETURN_IF_EXCEPTION(scope, { });
    if (!calendarProperty.isUndefined()) {
        throwTypeError(globalObject, scope, "argument object must not carry calendar"_s);
        return { };
    }

    JSValue timeZoneProperty = temporalTimeLike->get(globalObject, vm.propertyNames->timeZone);
    RETURN_IF_EXCEPTION(scope, { });
    if (!timeZoneProperty.isUndefined()) {
        throwTypeError(globalObject, scope, "argument object must not carry time zone"_s);
        return { };
    }

    auto [hourOptional, minuteOptional, secondOptional, millisecondOptional, microsecondOptional, nanosecondOptional] = toPartialTime(globalObject, temporalTimeLike);
    RETURN_IF_EXCEPTION(scope, { });

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

    TemporalOverflow overflow = toTemporalOverflow(globalObject, options);
    RETURN_IF_EXCEPTION(scope, { });

    ISO8601::Duration duration { };
    duration.setHours(hourOptional.value_or(hour()));
    duration.setMinutes(minuteOptional.value_or(minute()));
    duration.setSeconds(secondOptional.value_or(second()));
    duration.setMilliseconds(millisecondOptional.value_or(millisecond()));
    duration.setMicroseconds(microsecondOptional.value_or(microsecond()));
    duration.setNanoseconds(nanosecondOptional.value_or(nanosecond()));

    RELEASE_AND_RETURN(scope, regulateTime(globalObject, WTFMove(duration), overflow));
}

static ISO8601::Duration differenceTime(ISO8601::PlainTime time1, ISO8601::PlainTime time2)
{
    double hours = static_cast<double>(time2.hour()) - static_cast<double>(time1.hour());
    double minutes = static_cast<double>(time2.minute()) - static_cast<double>(time1.minute());
    double seconds = static_cast<double>(time2.second()) - static_cast<double>(time1.second());
    double milliseconds = static_cast<double>(time2.millisecond()) - static_cast<double>(time1.millisecond());
    double microseconds = static_cast<double>(time2.microsecond()) - static_cast<double>(time1.microsecond());
    double nanoseconds = static_cast<double>(time2.nanosecond()) - static_cast<double>(time1.nanosecond());
    dataLogLnIf(TemporalPlainTimeInternal::verbose, "Diff ", hours, " ", minutes, " ", seconds, " ", milliseconds, " ", microseconds, " ", nanoseconds);
    int32_t sign = TemporalDuration::sign(ISO8601::Duration(0, 0, 0, 0, hours, minutes, seconds, milliseconds, microseconds, nanoseconds));
    auto duration = balanceTime(hours * sign, minutes * sign, seconds * sign, milliseconds * sign, microseconds * sign, nanoseconds * sign);
    dataLogLnIf(TemporalPlainTimeInternal::verbose, "Balanced ", duration.days(), " ", duration.hours(), " ", duration.minutes(), " ", duration.seconds(), " ", duration.milliseconds(), " ", duration.microseconds(), " ", duration.nanoseconds());
    if (sign == -1)
        return -duration;
    return duration;
}

static std::tuple<TemporalUnit, TemporalUnit, RoundingMode, double> extractDifferenceOptions(JSGlobalObject* globalObject, JSValue optionsValue)
{
    VM& vm = globalObject->vm();
    auto scope = DECLARE_THROW_SCOPE(vm);

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

    auto smallest = temporalSmallestUnit(globalObject, options, { TemporalUnit::Year, TemporalUnit::Month, TemporalUnit::Week, TemporalUnit::Day });
    RETURN_IF_EXCEPTION(scope, { });
    TemporalUnit smallestUnit = smallest.value_or(TemporalUnit::Nanosecond);

    auto largest = temporalLargestUnit(globalObject, options, { TemporalUnit::Year, TemporalUnit::Month, TemporalUnit::Week, TemporalUnit::Day }, TemporalUnit::Hour);
    RETURN_IF_EXCEPTION(scope, { });
    TemporalUnit largestUnit = largest.value_or(TemporalUnit::Hour);

    if (smallestUnit < largestUnit) {
        throwRangeError(globalObject, scope, "smallestUnit must be smaller than largestUnit"_s);
        return { };
    }

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

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

    return { smallestUnit, largestUnit, roundingMode, increment };
}

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

    auto [smallestUnit, largestUnit, roundingMode, increment] = extractDifferenceOptions(globalObject, optionsValue);
    RETURN_IF_EXCEPTION(scope, { });

    auto result = differenceTime(plainTime(), other->plainTime());
    result.setYears(0);
    result.setMonths(0);
    result.setWeeks(0);
    result.setDays(0);
    TemporalDuration::round(result, increment, smallestUnit, roundingMode);
    TemporalDuration::balance(result, largestUnit);
    return result;
}

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

    auto [smallestUnit, largestUnit, roundingMode, increment] = extractDifferenceOptions(globalObject, optionsValue);
    RETURN_IF_EXCEPTION(scope, { });

    // https://tc39.es/proposal-temporal/#sec-temporal-negatetemporalroundingmode
    if (roundingMode == RoundingMode::Ceil)
        roundingMode = RoundingMode::Floor;
    else if (roundingMode == RoundingMode::Floor)
        roundingMode = RoundingMode::Ceil;

    auto result = differenceTime(other->plainTime(), plainTime());
    result = -result;
    result.setYears(0);
    result.setMonths(0);
    result.setWeeks(0);
    result.setDays(0);
    TemporalDuration::round(result, increment, smallestUnit, roundingMode);
    result = -result;
    result.setDays(0);
    TemporalDuration::balance(result, largestUnit);
    return result;
}

} // namespace JSC
