/*
 * Copyright (c) 2021-2026, Tim Flynn <trflynn89@ladybird.org>
 *
 * SPDX-License-Identifier: BSD-2-Clause
 */

#include <LibJS/Runtime/Array.h>
#include <LibJS/Runtime/Date.h>
#include <LibJS/Runtime/Intl/DateTimeFormat.h>
#include <LibJS/Runtime/NativeFunction.h>
#include <LibJS/Runtime/Temporal/Instant.h>
#include <LibJS/Runtime/Temporal/PlainDate.h>
#include <LibJS/Runtime/Temporal/PlainDateTime.h>
#include <LibJS/Runtime/Temporal/PlainMonthDay.h>
#include <LibJS/Runtime/Temporal/PlainTime.h>
#include <LibJS/Runtime/Temporal/PlainYearMonth.h>
#include <LibJS/Runtime/Temporal/TimeZone.h>
#include <LibJS/Runtime/Temporal/ZonedDateTime.h>
#include <math.h>

namespace JS::Intl {

GC_DEFINE_ALLOCATOR(DateTimeFormat);

// 11 DateTimeFormat Objects, https://tc39.es/ecma402/#datetimeformat-objects
DateTimeFormat::DateTimeFormat(Object& prototype)
    : IntlObject(ConstructWithPrototypeTag::Tag, prototype)
{
}

void DateTimeFormat::visit_edges(Cell::Visitor& visitor)
{
    Base::visit_edges(visitor);
    visitor.visit(m_bound_format);
}

// 11.2.3 Internal slots, https://tc39.es/ecma402/#sec-intl.datetimeformat-internal-slots
ReadonlySpan<StringView> DateTimeFormat::relevant_extension_keys() const
{
    // The value of the [[RelevantExtensionKeys]] internal slot is « "ca", "hc", "nu" ».
    static constexpr AK::Array keys { "ca"sv, "hc"sv, "nu"sv };
    return keys;
}

// 11.2.3 Internal slots, https://tc39.es/ecma402/#sec-intl.datetimeformat-internal-slots
ReadonlySpan<ResolutionOptionDescriptor> DateTimeFormat::resolution_option_descriptors(VM& vm) const
{
    // The value of the [[ResolutionOptionDescriptors]] internal slot is « { [[Key]]: "ca", [[Property]]: "calendar" }, { [[Key]]: "nu", [[Property]]: "numberingSystem" }, { [[Key]]: "hour12", [[Property]]: "hour12", [[Type]]: boolean }, { [[Key]]: "hc", [[Property]]: "hourCycle", [[Values]]: « "h11", "h12", "h23", "h24" » } ».
    static constexpr AK::Array hour_cycle_values { "h11"sv, "h12"sv, "h23"sv, "h24"sv };

    static auto descriptors = to_array<ResolutionOptionDescriptor>({
        { .key = "ca"sv, .property = vm.names.calendar },
        { .key = "nu"sv, .property = vm.names.numberingSystem },
        { .key = "hour12"sv, .property = vm.names.hour12, .type = OptionType::Boolean },
        { .key = "hc"sv, .property = vm.names.hourCycle, .values = hour_cycle_values },
    });

    return descriptors;
}

static Optional<Unicode::DateTimeFormat const&> get_or_create_formatter(StringView locale, StringView time_zone, OwnPtr<Unicode::DateTimeFormat>& formatter, Optional<Unicode::CalendarPattern> const& format)
{
    if (formatter)
        return *formatter;
    if (!format.has_value())
        return {};

    formatter = Unicode::DateTimeFormat::create_for_pattern_options(locale, time_zone, *format);
    return *formatter;
}

Optional<Unicode::DateTimeFormat const&> DateTimeFormat::temporal_plain_date_formatter()
{
    return get_or_create_formatter(m_icu_locale, "GMT+00:00"sv, m_temporal_plain_date_formatter, m_temporal_plain_date_format);
}

Optional<Unicode::DateTimeFormat const&> DateTimeFormat::temporal_plain_year_month_formatter()
{
    return get_or_create_formatter(m_icu_locale, "GMT+00:00"sv, m_temporal_plain_year_month_formatter, m_temporal_plain_year_month_format);
}

Optional<Unicode::DateTimeFormat const&> DateTimeFormat::temporal_plain_month_day_formatter()
{
    return get_or_create_formatter(m_icu_locale, "GMT+00:00"sv, m_temporal_plain_month_day_formatter, m_temporal_plain_month_day_format);
}

Optional<Unicode::DateTimeFormat const&> DateTimeFormat::temporal_plain_time_formatter()
{
    return get_or_create_formatter(m_icu_locale, "GMT+00:00"sv, m_temporal_plain_time_formatter, m_temporal_plain_time_format);
}

Optional<Unicode::DateTimeFormat const&> DateTimeFormat::temporal_plain_date_time_formatter()
{
    return get_or_create_formatter(m_icu_locale, "GMT+00:00"sv, m_temporal_plain_date_time_formatter, m_temporal_plain_date_time_format);
}

Optional<Unicode::DateTimeFormat const&> DateTimeFormat::temporal_instant_formatter()
{
    return get_or_create_formatter(m_icu_locale, m_temporal_time_zone, m_temporal_instant_formatter, m_temporal_instant_format);
}

// 11.5.5 FormatDateTimePattern ( dateTimeFormat, patternParts, x, rangeFormatOptions ), https://tc39.es/ecma402/#sec-formatdatetimepattern
// 15.6.4 FormatDateTimePattern ( dateTimeFormat, format, pattern, epochNanoseconds, isPlain ), https://tc39.es/proposal-temporal/#sec-formatdatetimepattern
Vector<Unicode::DateTimeFormat::Partition> format_date_time_pattern(ValueFormat const& format_record)
{
    return format_record.formatter.format_to_parts(format_record.epoch_milliseconds);
}

// 11.5.6 PartitionDateTimePattern ( dateTimeFormat, x ), https://tc39.es/ecma402/#sec-partitiondatetimepattern
// 15.6.5 PartitionDateTimePattern ( dateTimeFormat, x ), https://tc39.es/proposal-temporal/#sec-partitiondatetimepattern
ThrowCompletionOr<Vector<Unicode::DateTimeFormat::Partition>> partition_date_time_pattern(VM& vm, DateTimeFormat& date_time_format, FormattableDateTime const& time)
{
    // 1. Let xFormatRecord be ? HandleDateTimeValue(dateTimeFormat, x).
    auto format_record = TRY(handle_date_time_value(vm, date_time_format, time));

    // 5. Let result be FormatDateTimePattern(dateTimeFormat, format, pattern, epochNanoseconds, formatRecord.[[IsPlain]]).
    return format_date_time_pattern(format_record);
}

// 11.5.7 FormatDateTime ( dateTimeFormat, x ), https://tc39.es/ecma402/#sec-formatdatetime
// 15.6.6 FormatDateTime ( dateTimeFormat, x ), https://tc39.es/proposal-temporal/#sec-formatdatetime
ThrowCompletionOr<Utf16String> format_date_time(VM& vm, DateTimeFormat& date_time_format, FormattableDateTime const& time)
{
    // 1. Let parts be ? PartitionDateTimePattern(dateTimeFormat, x).
    // 2. Let result be the empty String.
    Utf16String result;

    // NOTE: We short-circuit PartitionDateTimePattern as we do not need individual partitions.
    {
        // 1. Let xFormatRecord be ? HandleDateTimeValue(dateTimeFormat, x).
        auto format_record = TRY(handle_date_time_value(vm, date_time_format, time));

        result = format_record.formatter.format(format_record.epoch_milliseconds);
    }

    // 4. Return result.
    return result;
}

// 11.5.8 FormatDateTimeToParts ( dateTimeFormat, x ), https://tc39.es/ecma402/#sec-formatdatetimetoparts
// 15.6.7 FormatDateTimeToParts ( dateTimeFormat, x ), https://tc39.es/proposal-temporal/#sec-formatdatetimetoparts
ThrowCompletionOr<GC::Ref<Array>> format_date_time_to_parts(VM& vm, DateTimeFormat& date_time_format, FormattableDateTime const& time)
{
    auto& realm = *vm.current_realm();

    // 1. Let parts be ? PartitionDateTimePattern(dateTimeFormat, x).
    auto parts = TRY(partition_date_time_pattern(vm, date_time_format, time));

    // 2. Let result be ! ArrayCreate(0).
    auto result = MUST(Array::create(realm, 0));

    // 3. Let n be 0.
    size_t n = 0;

    // 4. For each Record { [[Type]], [[Value]] } part in parts, do
    for (auto& part : parts) {
        // a. Let O be OrdinaryObjectCreate(%Object.prototype%).
        auto object = Object::create(realm, realm.intrinsics().object_prototype());

        // b. Perform ! CreateDataPropertyOrThrow(O, "type", part.[[Type]]).
        MUST(object->create_data_property_or_throw(vm.names.type, PrimitiveString::create(vm, part.type)));

        // c. Perform ! CreateDataPropertyOrThrow(O, "value", part.[[Value]]).
        MUST(object->create_data_property_or_throw(vm.names.value, PrimitiveString::create(vm, move(part.value))));

        // d. Perform ! CreateDataProperty(result, ! ToString(n), O).
        MUST(result->create_data_property_or_throw(n, object));

        // e. Increment n by 1.
        ++n;
    }

    // 5. Return result.
    return result;
}

// 11.5.9 PartitionDateTimeRangePattern ( dateTimeFormat, x, y ), https://tc39.es/ecma402/#sec-partitiondatetimerangepattern
// 15.6.8 PartitionDateTimeRangePattern ( dateTimeFormat, x, y ), https://tc39.es/proposal-temporal/#sec-partitiondatetimerangepattern
ThrowCompletionOr<Vector<Unicode::DateTimeFormat::Partition>> partition_date_time_range_pattern(VM& vm, DateTimeFormat& date_time_format, FormattableDateTime const& start, FormattableDateTime const& end)
{
    // 1. If IsTemporalObject(x) is true or IsTemporalObject(y) is true, then
    if (is_temporal_object(start) || is_temporal_object(end)) {
        // a. If SameTemporalType(x, y) is false, throw a TypeError exception.
        if (!same_temporal_type(start, end))
            return vm.throw_completion<TypeError>(ErrorType::IntlTemporalFormatRangeTypeMismatch);
    }

    // 2. Let xFormatRecord be ? HandleDateTimeValue(dateTimeFormat, x).
    auto start_format_record = TRY(handle_date_time_value(vm, date_time_format, start));

    // 3. Let yFormatRecord be ? HandleDateTimeValue(dateTimeFormat, y).
    auto end_format_record = TRY(handle_date_time_value(vm, date_time_format, end));

    return start_format_record.formatter.format_range_to_parts(start_format_record.epoch_milliseconds, end_format_record.epoch_milliseconds);
}

// 11.5.10 FormatDateTimeRange ( dateTimeFormat, x, y ), https://tc39.es/ecma402/#sec-formatdatetimerange
// 15.6.9 FormatDateTimeRange ( dateTimeFormat, x, y ), https://tc39.es/proposal-temporal/#sec-formatdatetimerange
ThrowCompletionOr<Utf16String> format_date_time_range(VM& vm, DateTimeFormat& date_time_format, FormattableDateTime const& start, FormattableDateTime const& end)
{
    // 1. Let parts be ? PartitionDateTimeRangePattern(dateTimeFormat, x, y).
    // 2. Let result be the empty String.
    Utf16String result;

    // NOTE: We short-circuit PartitionDateTimeRangePattern as we do not need individual partitions.
    {
        // 1. If IsTemporalObject(x) is true or IsTemporalObject(y) is true, then
        if (is_temporal_object(start) || is_temporal_object(end)) {
            // a. If SameTemporalType(x, y) is false, throw a TypeError exception.
            if (!same_temporal_type(start, end))
                return vm.throw_completion<TypeError>(ErrorType::IntlTemporalFormatRangeTypeMismatch);
        }

        // 2. Let xFormatRecord be ? HandleDateTimeValue(dateTimeFormat, x).
        auto start_format_record = TRY(handle_date_time_value(vm, date_time_format, start));

        // 3. Let yFormatRecord be ? HandleDateTimeValue(dateTimeFormat, y).
        auto end_format_record = TRY(handle_date_time_value(vm, date_time_format, end));

        result = start_format_record.formatter.format_range(start_format_record.epoch_milliseconds, end_format_record.epoch_milliseconds);
    }

    // 4. Return result.
    return result;
}

// 11.5.11 FormatDateTimeRangeToParts ( dateTimeFormat, x, y ), https://tc39.es/ecma402/#sec-formatdatetimerangetoparts
// 15.6.10 FormatDateTimeRangeToParts ( dateTimeFormat, x, y ), https://tc39.es/proposal-temporal/#sec-formatdatetimerangetoparts
ThrowCompletionOr<GC::Ref<Array>> format_date_time_range_to_parts(VM& vm, DateTimeFormat& date_time_format, FormattableDateTime const& start, FormattableDateTime const& end)
{
    auto& realm = *vm.current_realm();

    // 1. Let parts be ? PartitionDateTimeRangePattern(dateTimeFormat, x, y).
    auto parts = TRY(partition_date_time_range_pattern(vm, date_time_format, start, end));

    // 2. Let result be ! ArrayCreate(0).
    auto result = MUST(Array::create(realm, 0));

    // 3. Let n be 0.
    size_t n = 0;

    // 4. For each Record { [[Type]], [[Value]], [[Source]] } part in parts, do
    for (auto& part : parts) {
        // a. Let O be OrdinaryObjectCreate(%ObjectPrototype%).
        auto object = Object::create(realm, realm.intrinsics().object_prototype());

        // b. Perform ! CreateDataPropertyOrThrow(O, "type", part.[[Type]]).
        MUST(object->create_data_property_or_throw(vm.names.type, PrimitiveString::create(vm, part.type)));

        // c. Perform ! CreateDataPropertyOrThrow(O, "value", part.[[Value]]).
        MUST(object->create_data_property_or_throw(vm.names.value, PrimitiveString::create(vm, move(part.value))));

        // d. Perform ! CreateDataPropertyOrThrow(O, "source", part.[[Source]]).
        MUST(object->create_data_property_or_throw(vm.names.source, PrimitiveString::create(vm, part.source)));

        // e. Perform ! CreateDataProperty(result, ! ToString(n), O).
        MUST(result->create_data_property_or_throw(n, object));

        // f. Increment n by 1.
        ++n;
    }

    // 5. Return result.
    return result;
}

// 15.6.1 GetDateTimeFormat ( formats, matcher, options, required, defaults, inherit ), https://tc39.es/proposal-temporal/#sec-getdatetimeformat
Optional<Unicode::CalendarPattern> get_date_time_format(Unicode::CalendarPattern const& options, OptionRequired required, OptionDefaults defaults, OptionInherit inherit)
{
    using enum Unicode::CalendarPattern::Field;

    auto required_options = [&]() -> ReadonlySpan<Unicode::CalendarPattern::Field> {
        static constexpr auto date_fields = AK::Array { Weekday, Year, Month, Day };
        static constexpr auto time_fields = AK::Array { DayPeriod, Hour, Minute, Second, FractionalSecondDigits };
        static constexpr auto year_month_fields = AK::Array { Year, Month };
        static constexpr auto month_day_fields = AK::Array { Month, Day };
        static constexpr auto any_fields = AK::Array { Weekday, Year, Month, Day, DayPeriod, Hour, Minute, Second, FractionalSecondDigits };

        switch (required) {
        // 1. If required is DATE, then
        case OptionRequired::Date:
            // a. Let requiredOptions be « "weekday", "year", "month", "day" ».
            return date_fields;
        // 2. Else if required is TIME, then
        case OptionRequired::Time:
            // a. Let requiredOptions be « "dayPeriod", "hour", "minute", "second", "fractionalSecondDigits" ».
            return time_fields;
        // 3. Else if required is YEAR-MONTH, then
        case OptionRequired::YearMonth:
            // a. Let requiredOptions be « "year", "month" ».
            return year_month_fields;
        // 4. Else if required is MONTH-DAY, then
        case OptionRequired::MonthDay:
            // a. Let requiredOptions be « "month", "day" ».
            return month_day_fields;
        // 5. Else,
        case OptionRequired::Any:
            // a. Assert: required is ANY.
            // b. Let requiredOptions be « "weekday", "year", "month", "day", "dayPeriod", "hour", "minute", "second", "fractionalSecondDigits" ».
            return any_fields;
        }
        VERIFY_NOT_REACHED();
    }();

    auto default_options = [&]() -> ReadonlySpan<Unicode::CalendarPattern::Field> {
        static constexpr auto date_fields = AK::Array { Year, Month, Day };
        static constexpr auto time_fields = AK::Array { Hour, Minute, Second };
        static constexpr auto year_month_fields = AK::Array { Year, Month };
        static constexpr auto month_day_fields = AK::Array { Month, Day };
        static constexpr auto all_fields = AK::Array { Year, Month, Day, Hour, Minute, Second };

        switch (defaults) {
        // 6. If defaults is DATE, then
        case OptionDefaults::Date:
            // a. Let defaultOptions be « "year", "month", "day" ».
            return date_fields;
        // 7. Else if defaults is TIME, then
        case OptionDefaults::Time:
            // a. Let defaultOptions be « "hour", "minute", "second" ».
            return time_fields;
        // 8. Else if defaults is YEAR-MONTH, then
        case OptionDefaults::YearMonth:
            // a. Let defaultOptions be « "year", "month" ».
            return year_month_fields;
        // 9. Else if defaults is MONTH-DAY, then
        case OptionDefaults::MonthDay:
            // a. Let defaultOptions be « "month", "day" ».
            return month_day_fields;
        // 10. Else,
        case OptionDefaults::ZonedDateTime:
        case OptionDefaults::All:
            // a. Assert: defaults is ZONED-DATE-TIME or ALL.
            // b. Let defaultOptions be « "year", "month", "day", "hour", "minute", "second" ».
            return all_fields;
        }
        VERIFY_NOT_REACHED();
    }();

    Unicode::CalendarPattern format_options {};

    // 11. If inherit is ALL, then
    if (inherit == OptionInherit::All) {
        // a. Let formatOptions be a copy of options.
        format_options = options;
    }
    // 12. Else,
    else {
        // a. Let formatOptions be a new Record.

        // b. If required is one of DATE, YEAR-MONTH, or ANY, then
        if (required == OptionRequired::Date || required == OptionRequired::YearMonth || required == OptionRequired::Any) {
            // i. Set formatOptions.[[era]] to options.[[era]].
            format_options.era = options.era;
        }

        // c. If required is either TIME or ANY, then
        if (required == OptionRequired::Time || required == OptionRequired::Any) {
            // i. Set formatOptions.[[hourCycle]] to options.[[hourCycle]].
            format_options.hour_cycle = options.hour_cycle;
            format_options.hour12 = options.hour12;
        }
    }

    // 13. Let anyPresent be false.
    auto any_present = false;

    // 14. For each property name prop of « "weekday", "year", "month", "day", "dayPeriod", "hour", "minute", "second", "fractionalSecondDigits" », do
    static constexpr auto all_fields = AK::Array { Weekday, Year, Month, Day, DayPeriod, Hour, Minute, Second, FractionalSecondDigits };

    options.for_each_calendar_field_zipped_with(format_options, all_fields, [&](auto const& option, auto&) {
        // a. If options.[[<prop>]] is not undefined, set anyPresent to true.
        if (option.has_value()) {
            any_present = true;
            return IterationDecision::Break;
        }

        return IterationDecision::Continue;
    });

    // 15. Let needDefaults be true.
    auto need_defaults = true;

    // 16. For each property name prop of requiredOptions, do
    options.for_each_calendar_field_zipped_with(format_options, required_options, [&](auto const& option, auto& format_option) {
        // a. Let value be options.[[<prop>]].
        // b. If value is not undefined, then
        if (option.has_value()) {
            // i. Set formatOptions.[[<prop>]] to value.
            format_option = *option;

            // ii. Set needDefaults to false.
            need_defaults = false;
        }

        return IterationDecision::Continue;
    });

    // 17. If needDefaults is true, then
    if (need_defaults) {
        // a. If anyPresent is true and inherit is RELEVANT, return null.
        if (any_present && inherit == OptionInherit::Relevant)
            return {};

        // b. For each property name prop of defaultOptions, do
        options.for_each_calendar_field_zipped_with(format_options, default_options, [&](auto const&, auto& format_option) {
            using ValueType = typename RemoveCVReference<decltype(format_option)>::ValueType;

            if constexpr (IsSame<ValueType, Unicode::CalendarPatternStyle>) {
                // i. Set formatOptions.[[<prop>]] to "numeric".
                format_option = Unicode::CalendarPatternStyle::Numeric;
            }

            return IterationDecision::Continue;
        });

        // c. If defaults is ZONED-DATE-TIME and formatOptions.[[timeZoneName]] is undefined, then
        if (defaults == OptionDefaults::ZonedDateTime && !format_options.time_zone_name.has_value()) {
            // i. Set formatOptions.[[timeZoneName]] to "short".
            format_options.time_zone_name = Unicode::CalendarPatternStyle::Short;
        }
    }

    // 18. If matcher is "basic", then
    //     a. Let bestFormat be BasicFormatMatcher(formatOptions, formats).
    // 19. Else,
    //     a. Let bestFormat be BestFitFormatMatcher(formatOptions, formats).
    // 20. Return bestFormat.
    return format_options;
}

// 15.6.2 AdjustDateTimeStyleFormat ( formats, baseFormat, matcher, allowedOptions ), https://tc39.es/proposal-temporal/#sec-adjustdatetimestyleformat
Unicode::CalendarPattern adjust_date_time_style_format(VM& vm, Unicode::CalendarPattern& base_format, ReadonlySpan<Unicode::CalendarPattern::Field> allowed_options)
{
    // 1. Let anyConflictingFields be false.
    auto any_conflicted_fields = false;

    // 2. For each row of Table 16, except the header row, in table order, do
    MUST(for_each_calendar_field(vm, base_format, [&](auto option_type, auto& option, auto const&, auto const&) -> ThrowCompletionOr<void> {
        // a. Let prop be the name given in the "Property" column of the current row.
        // b. If baseFormat has a [[<prop>]] field and allowedOptions does not contain prop, set anyConflictingFields to true.
        if (option.has_value() && !allowed_options.contains_slow(option_type))
            any_conflicted_fields = true;
        return {};
    }));

    // 3. If anyConflictingFields is false, return baseFormat.
    if (!any_conflicted_fields)
        return base_format;

    // 4. NOTE: The above steps prevent the operation from returning an altered format when baseFormat would be sufficient.
    //    This should be unnecessary, but exists because the ECMA-402 specification does not guarantee that a format
    //    returned from DateTimeStyleFormat can also be returned from BasicFormatMatcher or BestFitFormatMatcher.

    // 5. Let formatOptions be a new Record.
    Unicode::CalendarPattern format_options;

    // 6. For each property name prop of allowedOptions, do
    base_format.for_each_calendar_field_zipped_with(format_options, allowed_options, [&](auto const& base_option, auto& format_option) {
        // a. If baseFormat has a [[<prop>]] field, set formatOptions.[[<prop>]] to baseFormat.[[<prop>]].
        if (base_option.has_value())
            format_option = base_option;
        return IterationDecision::Continue;
    });

    // 7. If matcher is "basic", then
    //     a. Let bestFormat be BasicFormatMatcher(formatOptions, formats).
    // 8. Else,
    //     a. Let bestFormat be BestFitFormatMatcher(formatOptions, formats).
    // 9. Return bestFormat.
    return format_options;
}

// 15.6.11 ToDateTimeFormattable ( value ), https://tc39.es/proposal-temporal/#sec-todatetimeformattable
ThrowCompletionOr<FormattableDateTime> to_date_time_formattable(VM& vm, Value value)
{
    // 1. If IsTemporalObject(value) is true, return value.
    if (auto instant = value.as_if<Temporal::Instant>())
        return FormattableDateTime { *instant };
    if (auto plain_date = value.as_if<Temporal::PlainDate>())
        return FormattableDateTime { *plain_date };
    if (auto plain_date_time = value.as_if<Temporal::PlainDateTime>())
        return FormattableDateTime { *plain_date_time };
    if (auto plain_month_day = value.as_if<Temporal::PlainMonthDay>())
        return FormattableDateTime { *plain_month_day };
    if (auto plain_time = value.as_if<Temporal::PlainTime>())
        return FormattableDateTime { *plain_time };
    if (auto plain_year_month = value.as_if<Temporal::PlainYearMonth>())
        return FormattableDateTime { *plain_year_month };
    if (auto zoned_date_time = value.as_if<Temporal::ZonedDateTime>())
        return FormattableDateTime { *zoned_date_time };

    // 2. Return ? ToNumber(value).
    return FormattableDateTime { TRY(value.to_number(vm)).as_double() };
}

// 15.6.12 IsTemporalObject ( value ), https://tc39.es/proposal-temporal/#sec-temporal-istemporalobject
bool is_temporal_object(FormattableDateTime const& value)
{
    // 1. If value is not an Object, return false.
    // 2. If value does not have an [[InitializedTemporalDate]], [[InitializedTemporalTime]], [[InitializedTemporalDateTime]],
    //    [[InitializedTemporalZonedDateTime]], [[InitializedTemporalYearMonth]], [[InitializedTemporalMonthDay]], or
    //    [[InitializedTemporalInstant]] internal slot, return false.
    // 3. Return true.
    return !value.has<double>();
}

// 15.6.13 SameTemporalType ( x, y ), https://tc39.es/proposal-temporal/#sec-temporal-istemporalobject
bool same_temporal_type(FormattableDateTime const& x, FormattableDateTime const& y)
{
    // 1. If either of IsTemporalObject(x) or IsTemporalObject(y) is false, return false.
    if (!is_temporal_object(x) || !is_temporal_object(y))
        return false;

    // 2. If x has an [[InitializedTemporalDate]] internal slot and y does not, return false.
    // 3. If x has an [[InitializedTemporalTime]] internal slot and y does not, return false.
    // 4. If x has an [[InitializedTemporalDateTime]] internal slot and y does not, return false.
    // 5. If x has an [[InitializedTemporalZonedDateTime]] internal slot and y does not, return false.
    // 6. If x has an [[InitializedTemporalYearMonth]] internal slot and y does not, return false.
    // 7. If x has an [[InitializedTemporalMonthDay]] internal slot and y does not, return false.
    // 8. If x has an [[InitializedTemporalInstant]] internal slot and y does not, return false.
    // 9. Return true.
    return x.index() == y.index();
}

static double to_epoch_milliseconds(Crypto::SignedBigInteger const& epoch_nanoseconds)
{
    return big_floor(epoch_nanoseconds, Temporal::NANOSECONDS_PER_MILLISECOND).to_double();
}

// 15.6.15 HandleDateTimeTemporalDate ( dateTimeFormat, temporalDate ), https://tc39.es/proposal-temporal/#sec-temporal-handledatetimetemporaldate
ThrowCompletionOr<ValueFormat> handle_date_time_temporal_date(VM& vm, DateTimeFormat& date_time_format, Temporal::PlainDate const& temporal_date)
{
    // 1. If temporalDate.[[Calendar]] is not either dateTimeFormat.[[Calendar]] or "iso8601", throw a RangeError exception.
    if (!temporal_date.calendar().is_one_of(date_time_format.calendar(), "iso8601"sv))
        return vm.throw_completion<RangeError>(ErrorType::IntlTemporalInvalidCalendar, "Temporal.PlainDate"sv, temporal_date.calendar(), date_time_format.calendar());

    // 2. Let isoDateTime be CombineISODateAndTimeRecord(temporalDate.[[ISODate]], NoonTimeRecord()).
    auto iso_date_time = Temporal::combine_iso_date_and_time_record(temporal_date.iso_date(), Temporal::noon_time_record());

    // 3. Let epochNs be GetUTCEpochNanoseconds(isoDateTime).
    auto epoch_nanoseconds = get_utc_epoch_nanoseconds(iso_date_time);

    // 4. Let format be dateTimeFormat.[[TemporalPlainDateFormat]].
    auto formatter = date_time_format.temporal_plain_date_formatter();

    // 5. If format is null, throw a TypeError exception.
    if (!formatter.has_value())
        return vm.throw_completion<TypeError>(ErrorType::IntlTemporalFormatIsNull, "Temporal.PlainDate"sv);

    // 6. Return Value Format Record { [[Format]]: format, [[EpochNanoseconds]]: epochNs, [[IsPlain]]: true  }.
    return ValueFormat { .formatter = *formatter, .epoch_milliseconds = to_epoch_milliseconds(epoch_nanoseconds) };
}

// 15.6.16 HandleDateTimeTemporalYearMonth ( dateTimeFormat, temporalYearMonth ), https://tc39.es/proposal-temporal/#sec-temporal-handledatetimetemporalyearmonth
ThrowCompletionOr<ValueFormat> handle_date_time_temporal_year_month(VM& vm, DateTimeFormat& date_time_format, Temporal::PlainYearMonth const& temporal_year_month)
{
    // 1. If temporalYearMonth.[[Calendar]] is not equal to dateTimeFormat.[[Calendar]], throw a RangeError exception.
    if (temporal_year_month.calendar() != date_time_format.calendar())
        return vm.throw_completion<RangeError>(ErrorType::IntlTemporalInvalidCalendar, "Temporal.PlainYearMonth"sv, temporal_year_month.calendar(), date_time_format.calendar());

    // 2. Let isoDateTime be CombineISODateAndTimeRecord(temporalYearMonth.[[ISODate]], NoonTimeRecord()).
    auto iso_date_time = Temporal::combine_iso_date_and_time_record(temporal_year_month.iso_date(), Temporal::noon_time_record());

    // 3. Let epochNs be GetUTCEpochNanoseconds(isoDateTime).
    auto epoch_nanoseconds = get_utc_epoch_nanoseconds(iso_date_time);

    // 4. Let format be dateTimeFormat.[[TemporalPlainYearMonthFormat]].
    auto formatter = date_time_format.temporal_plain_year_month_formatter();

    // 5. If format is null, throw a TypeError exception.
    if (!formatter.has_value())
        return vm.throw_completion<TypeError>(ErrorType::IntlTemporalFormatIsNull, "Temporal.PlainYearMonth"sv);

    // 6. Return Value Format Record { [[Format]]: format, [[EpochNanoseconds]]: epochNs, [[IsPlain]]: true  }.
    return ValueFormat { .formatter = *formatter, .epoch_milliseconds = to_epoch_milliseconds(epoch_nanoseconds) };
}

// 15.6.17 HandleDateTimeTemporalMonthDay ( dateTimeFormat, temporalMonthDay ), https://tc39.es/proposal-temporal/#sec-temporal-handledatetimetemporalmonthday
ThrowCompletionOr<ValueFormat> handle_date_time_temporal_month_day(VM& vm, DateTimeFormat& date_time_format, Temporal::PlainMonthDay const& temporal_month_day)
{
    // 1. If temporalMonthDay.[[Calendar]] is not equal to dateTimeFormat.[[Calendar]], throw a RangeError exception.
    if (temporal_month_day.calendar() != date_time_format.calendar())
        return vm.throw_completion<RangeError>(ErrorType::IntlTemporalInvalidCalendar, "Temporal.PlainMonthDay"sv, temporal_month_day.calendar(), date_time_format.calendar());

    // 2. Let isoDateTime be CombineISODateAndTimeRecord(temporalMonthDay.[[ISODate]], NoonTimeRecord()).
    auto iso_date_time = Temporal::combine_iso_date_and_time_record(temporal_month_day.iso_date(), Temporal::noon_time_record());

    // 3. Let epochNs be GetUTCEpochNanoseconds(isoDateTime).
    auto epoch_nanoseconds = get_utc_epoch_nanoseconds(iso_date_time);

    // 4. Let format be dateTimeFormat.[[TemporalPlainMonthDayFormat]].
    auto formatter = date_time_format.temporal_plain_month_day_formatter();

    // 5. If format is null, throw a TypeError exception.
    if (!formatter.has_value())
        return vm.throw_completion<TypeError>(ErrorType::IntlTemporalFormatIsNull, "Temporal.PlainMonthDay"sv);

    // 6. Return Value Format Record { [[Format]]: format, [[EpochNanoseconds]]: epochNs, [[IsPlain]]: true  }.
    return ValueFormat { .formatter = *formatter, .epoch_milliseconds = to_epoch_milliseconds(epoch_nanoseconds) };
}

// 15.6.18 HandleDateTimeTemporalTime ( dateTimeFormat, temporalTime ), https://tc39.es/proposal-temporal/#sec-temporal-handledatetimetemporaltime
ThrowCompletionOr<ValueFormat> handle_date_time_temporal_time(VM& vm, DateTimeFormat& date_time_format, Temporal::PlainTime const& temporal_time)
{
    // 1. Let isoDate be CreateISODateRecord(1970, 1, 1).
    auto iso_date = Temporal::create_iso_date_record(1970, 1, 1);

    // 2. Let isoDateTime be CombineISODateAndTimeRecord(isoDate, temporalTime.[[Time]]).
    auto iso_date_time = Temporal::combine_iso_date_and_time_record(iso_date, temporal_time.time());

    // 3. Let epochNs be GetUTCEpochNanoseconds(isoDateTime).
    auto epoch_nanoseconds = get_utc_epoch_nanoseconds(iso_date_time);

    // 4. Let format be dateTimeFormat.[[TemporalPlainTimeFormat]].
    auto formatter = date_time_format.temporal_plain_time_formatter();

    // 5. If format is null, throw a TypeError exception.
    if (!formatter.has_value())
        return vm.throw_completion<TypeError>(ErrorType::IntlTemporalFormatIsNull, "Temporal.PlainTime"sv);

    // 6. Return Value Format Record { [[Format]]: format, [[EpochNanoseconds]]: epochNs, [[IsPlain]]: true  }.
    return ValueFormat { .formatter = *formatter, .epoch_milliseconds = to_epoch_milliseconds(epoch_nanoseconds) };
}

// 15.6.19 HandleDateTimeTemporalDateTime ( dateTimeFormat, dateTime ), https://tc39.es/proposal-temporal/#sec-temporal-handledatetimetemporaldatetime
ThrowCompletionOr<ValueFormat> handle_date_time_temporal_date_time(VM& vm, DateTimeFormat& date_time_format, Temporal::PlainDateTime const& date_time)
{
    // 1. If dateTime.[[Calendar]] is not "iso8601" and not equal to dateTimeFormat.[[Calendar]], throw a RangeError exception.
    if (!date_time.calendar().is_one_of(date_time_format.calendar(), "iso8601"sv))
        return vm.throw_completion<RangeError>(ErrorType::IntlTemporalInvalidCalendar, "Temporal.PlainDateTime"sv, date_time.calendar(), date_time_format.calendar());

    // 2. Let epochNs be GetUTCEpochNanoseconds(dateTime.[[ISODateTime]]).
    auto epoch_nanoseconds = get_utc_epoch_nanoseconds(date_time.iso_date_time());

    // 3. Let format be dateTimeFormat.[[TemporalPlainDateTimeFormat]].
    auto formatter = date_time_format.temporal_plain_date_time_formatter();
    VERIFY(formatter.has_value());

    // 4. Return Value Format Record { [[Format]]: format, [[EpochNanoseconds]]: epochNs, [[IsPlain]]: true  }.
    return ValueFormat { .formatter = *formatter, .epoch_milliseconds = to_epoch_milliseconds(epoch_nanoseconds) };
}

// 15.6.20 HandleDateTimeTemporalInstant ( dateTimeFormat, instant ), https://tc39.es/proposal-temporal/#sec-temporal-handledatetimetemporalinstant
ValueFormat handle_date_time_temporal_instant(DateTimeFormat& date_time_format, Temporal::Instant const& instant)
{
    // 1. Let format be dateTimeFormat.[[TemporalInstantFormat]].
    auto formatter = date_time_format.temporal_instant_formatter();
    VERIFY(formatter.has_value());

    // 2. Return Value Format Record { [[Format]]: format, [[EpochNanoseconds]]: instant.[[EpochNanoseconds]], [[IsPlain]]: false  }.
    return ValueFormat { .formatter = *formatter, .epoch_milliseconds = to_epoch_milliseconds(instant.epoch_nanoseconds()->big_integer()) };
}

// 15.6.21 HandleDateTimeOthers ( dateTimeFormat, x ), https://tc39.es/proposal-temporal/#sec-temporal-handledatetimeothers
ThrowCompletionOr<ValueFormat> handle_date_time_others(VM& vm, DateTimeFormat& date_time_format, double time)
{
    // 1. Set x to TimeClip(x).
    time = time_clip(time);

    // 2. If x is NaN, throw a RangeError exception.
    if (isnan(time))
        return vm.throw_completion<RangeError>(ErrorType::IntlInvalidTime);

    // 3. Let epochNanoseconds be ℤ(ℝ(x) × 10**6).

    // 4. Let format be dateTimeFormat.[[DateTimeFormat]].
    auto const& formatter = date_time_format.formatter();

    // 5. Return Value Format Record { [[Format]]: format, [[EpochNanoseconds]]: epochNanoseconds, [[IsPlain]]: false  }.
    return ValueFormat { .formatter = formatter, .epoch_milliseconds = time };
}

// 15.6.22 HandleDateTimeValue ( dateTimeFormat, x ), https://tc39.es/proposal-temporal/#sec-temporal-handledatetimevalue
ThrowCompletionOr<ValueFormat> handle_date_time_value(VM& vm, DateTimeFormat& date_time_format, FormattableDateTime const& formattable)
{
    return formattable.visit(
        // 1. If x is a Number, return ? HandleDateTimeOthers(dateTimeFormat, x).
        [&](double time) {
            return handle_date_time_others(vm, date_time_format, time);
        },
        // 2. If x has an [[InitializedTemporalDate]] internal slot, return ? HandleDateTimeTemporalDate(dateTimeFormat, x).
        [&](GC::Ref<Temporal::PlainDate> temporal_date) {
            return handle_date_time_temporal_date(vm, date_time_format, temporal_date);
        },
        // 3. If x has an [[InitializedTemporalYearMonth]] internal slot, return ? HandleDateTimeTemporalYearMonth(dateTimeFormat, x).
        [&](GC::Ref<Temporal::PlainYearMonth> temporal_year_month) {
            return handle_date_time_temporal_year_month(vm, date_time_format, temporal_year_month);
        },
        // 4. If x has an [[InitializedTemporalMonthDay]] internal slot, return ? HandleDateTimeTemporalMonthDay(dateTimeFormat, x).
        [&](GC::Ref<Temporal::PlainMonthDay> temporal_month_day) {
            return handle_date_time_temporal_month_day(vm, date_time_format, temporal_month_day);
        },
        // 5. If x has an [[InitializedTemporalTime]] internal slot, return ? HandleDateTimeTemporalTime(dateTimeFormat, x).
        [&](GC::Ref<Temporal::PlainTime> temporal_time) {
            return handle_date_time_temporal_time(vm, date_time_format, temporal_time);
        },
        // 6. If x has an [[InitializedTemporalDateTime]] internal slot, return ? HandleDateTimeTemporalDateTime(dateTimeFormat, x).
        [&](GC::Ref<Temporal::PlainDateTime> date_time) {
            return handle_date_time_temporal_date_time(vm, date_time_format, date_time);
        },
        // 7. If x has an [[InitializedTemporalInstant]] internal slot, return HandleDateTimeTemporalInstant(dateTimeFormat, x).
        [&](GC::Ref<Temporal::Instant> instant) -> ThrowCompletionOr<ValueFormat> {
            return handle_date_time_temporal_instant(date_time_format, instant);
        },
        // 8. Assert: x has an [[InitializedTemporalZonedDateTime]] internal slot.
        [&](GC::Ref<Temporal::ZonedDateTime>) -> ThrowCompletionOr<ValueFormat> {
            // 9. Throw a TypeError exception.
            return vm.throw_completion<TypeError>(ErrorType::IntlTemporalZonedDateTime);
        });
}

}
