/*
 * Copyright (C) 2022 Apple Inc. All rights reserved.
 * Copyright (C) 2022 Sony Interactive Entertainment 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 "TemporalPlainDate.h"

#include "DateConstructor.h"
#include "IntlObjectInlines.h"
#include "JSCInlines.h"
#include "LazyPropertyInlines.h"
#include "TemporalDuration.h"
#include "TemporalPlainDateTime.h"
#include "VMTrapsInlines.h"

namespace JSC {

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

TemporalPlainDate* TemporalPlainDate::create(VM& vm, Structure* structure, ISO8601::PlainDate&& plainDate)
{
    auto* object = new (NotNull, allocateCell<TemporalPlainDate>(vm)) TemporalPlainDate(vm, structure, WTF::move(plainDate));
    object->finishCreation(vm);
    return object;
}

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

TemporalPlainDate::TemporalPlainDate(VM& vm, Structure* structure, ISO8601::PlainDate&& plainDate)
    : Base(vm, structure)
    , m_plainDate(WTF::move(plainDate))
{
}

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

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

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

DEFINE_VISIT_CHILDREN(TemporalPlainDate);

ISO8601::PlainDate TemporalPlainDate::toPlainDate(JSGlobalObject* globalObject, const ISO8601::Duration& duration)
{
    VM& vm = globalObject->vm();
    auto scope = DECLARE_THROW_SCOPE(vm);

    double yearDouble = duration.years();
    double monthDouble = duration.months();
    double dayDouble = duration.days();

    if (!ISO8601::isYearWithinLimits(yearDouble)) {
        throwRangeError(globalObject, scope, "year is out of range"_s);
        return { };
    }
    int32_t year = static_cast<int32_t>(yearDouble);

    if (!(monthDouble >= 1 && monthDouble <= 12)) {
        throwRangeError(globalObject, scope, "month is out of range"_s);
        return { };
    }
    unsigned month = static_cast<unsigned>(monthDouble);

    double daysInMonth = ISO8601::daysInMonth(year, month);
    if (!(dayDouble >= 1 && dayDouble <= daysInMonth)) {
        throwRangeError(globalObject, scope, "day is out of range"_s);
        return { };
    }
    unsigned day = static_cast<unsigned>(dayDouble);

    return ISO8601::PlainDate(year, month, day);
}

// CreateTemporalDate ( years, months, days )
// https://tc39.es/proposal-temporal/#sec-temporal-createtemporaldate
TemporalPlainDate* TemporalPlainDate::tryCreateIfValid(JSGlobalObject* globalObject, Structure* structure, ISO8601::PlainDate&& plainDate)
{
    VM& vm = globalObject->vm();
    auto scope = DECLARE_THROW_SCOPE(vm);

    if (!ISO8601::isDateTimeWithinLimits(plainDate.year(), plainDate.month(), plainDate.day(), 12, 0, 0, 0, 0, 0)) {
        throwRangeError(globalObject, scope, "date time is out of range of ECMAScript representation"_s);
        return { };
    }

    return TemporalPlainDate::create(vm, structure, WTF::move(plainDate));
}

TemporalPlainDate* TemporalPlainDate::tryCreateIfValid(JSGlobalObject* globalObject, Structure* structure, ISO8601::Duration&& duration)
{
    VM& vm = globalObject->vm();
    auto scope = DECLARE_THROW_SCOPE(vm);

    auto plainDate = toPlainDate(globalObject, duration);
    RETURN_IF_EXCEPTION(scope, { });

    RELEASE_AND_RETURN(scope, TemporalPlainDate::tryCreateIfValid(globalObject, structure,  WTF::move(plainDate)));
}

String TemporalPlainDate::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();

    return ISO8601::temporalDateToString(m_plainDate);
}

// https://tc39.es/proposal-temporal/#sec-temporal-totemporaldate
TemporalPlainDate* TemporalPlainDate::from(JSGlobalObject* globalObject, JSValue itemValue, Variant<JSObject*, TemporalOverflow> optionsOrOverflow)
{
    VM& vm = globalObject->vm();
    auto scope = DECLARE_THROW_SCOPE(vm);

    if (itemValue.isObject()) {
        if (itemValue.inherits<TemporalPlainDate>())
            return jsCast<TemporalPlainDate*>(itemValue);

        if (itemValue.inherits<TemporalPlainDateTime>())
            return TemporalPlainDate::create(vm, globalObject->plainDateStructure(), jsCast<TemporalPlainDateTime*>(itemValue)->plainDate());

        JSObject* calendar = TemporalCalendar::getTemporalCalendarWithISODefault(globalObject, itemValue);
        RETURN_IF_EXCEPTION(scope, { });

        // FIXME: Implement after fleshing out Temporal.Calendar.
        if (!calendar->inherits<TemporalCalendar>() || !jsCast<TemporalCalendar*>(calendar)->isISO8601()) {
            throwRangeError(globalObject, scope, "unimplemented: from non-ISO8601 calendar"_s);
            return { };
        }

        auto overflow = TemporalOverflow::Constrain;
        auto plainDate = TemporalCalendar::isoDateFromFields(globalObject, asObject(itemValue), TemporalDateFormat::Date, optionsOrOverflow, overflow);
        RETURN_IF_EXCEPTION(scope, { });
        return TemporalPlainDate::create(vm, globalObject->plainDateStructure(), WTF::move(plainDate));
    }

    if (!itemValue.isString()) {
        throwTypeError(globalObject, scope, "can only convert to PlainDate from object or string values"_s);
        return { };
    }

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

    // https://tc39.es/proposal-temporal/#sec-temporal-parsetemporaldatestring
    // TemporalDateString :
    //     CalendarDateTime
    auto dateTime = ISO8601::parseCalendarDateTime(string, TemporalDateFormat::Date);
    if (dateTime) {
        auto [plainDate, plainTimeOptional, timeZoneOptional, calendarOptional] = WTF::move(dateTime.value());
        if (!(timeZoneOptional && timeZoneOptional->m_z))
            RELEASE_AND_RETURN(scope, TemporalPlainDate::tryCreateIfValid(globalObject, globalObject->plainDateStructure(), WTF::move(plainDate)));
    }

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

// This operation is not in the spec, but does the same work as a combination of
// PrepareCalendarFields and CalendarMergeFields:
// https://tc39.es/proposal-temporal/#sec-temporal-preparecalendarfields
// https://tc39.es/proposal-temporal/#sec-temporal-calendarmergefields
// Needs to take a default year, month and day so that validity can be checked.
std::tuple<int32_t, unsigned, unsigned, std::optional<ParsedMonthCode>, TemporalOverflow, TemporalAnyProperties>
TemporalPlainDate::mergeDateFields(JSGlobalObject* globalObject, JSObject* temporalDateLike, JSValue optionsValue,
    int32_t defaultYear, unsigned defaultMonth, unsigned defaultDay)
{
    VM& vm = globalObject->vm();
    auto scope = DECLARE_THROW_SCOPE(vm);

    TemporalAnyProperties any = TemporalAnyProperties::None;

    std::optional<double> day;
    JSValue dayProperty = temporalDateLike->get(globalObject, vm.propertyNames->day);
    RETURN_IF_EXCEPTION(scope, { });
    if (!dayProperty.isUndefined()) {
        day = dayProperty.toIntegerOrInfinity(globalObject);
        RETURN_IF_EXCEPTION(scope, { });

        if (day.value() <= 0 || !std::isfinite(day.value())) [[unlikely]] {
            throwRangeError(globalObject, scope, "day property must be positive and finite"_s);
            return { };
        }

        any = TemporalAnyProperties::Some;
    }

    std::optional<double> month;
    JSValue monthProperty = temporalDateLike->get(globalObject, vm.propertyNames->month);
    RETURN_IF_EXCEPTION(scope, { });
    if (!monthProperty.isUndefined()) {
        month = monthProperty.toIntegerOrInfinity(globalObject);
        RETURN_IF_EXCEPTION(scope, { });

        if (month.value() <= 0 || !std::isfinite(month.value())) [[unlikely]] {
            throwRangeError(globalObject, scope, "month property must be positive and finite"_s);
            return { };
        }
        any = TemporalAnyProperties::Some;
    }

    JSValue monthCodeProperty = temporalDateLike->get(globalObject, vm.propertyNames->monthCode);
    RETURN_IF_EXCEPTION(scope, { });
    std::optional<ParsedMonthCode> otherMonth;
    bool monthCodePresent = false;
    if (!monthCodeProperty.isUndefined()) {
        auto monthCodeString = monthCodeProperty.toWTFString(globalObject);
        RETURN_IF_EXCEPTION(scope, { });

        otherMonth = ISO8601::parseMonthCode(monthCodeString);
        monthCodePresent = true;
        any = TemporalAnyProperties::Some;
    }

    std::optional<double> year;
    JSValue yearProperty = temporalDateLike->get(globalObject, vm.propertyNames->year);
    RETURN_IF_EXCEPTION(scope, { });
    if (!yearProperty.isUndefined()) {
        year = yearProperty.toIntegerOrInfinity(globalObject);
        RETURN_IF_EXCEPTION(scope, { });

        if (!std::isfinite(year.value())) [[unlikely]] {
            throwRangeError(globalObject, scope, "year property must be finite"_s);
            return { };
        }
        any = TemporalAnyProperties::Some;
    }

    if (monthCodePresent) {
        if (!otherMonth) [[unlikely]] {
            throwRangeError(globalObject, scope, "Invalid monthCode property"_s);
            return { };
        }
        if (!month)
            month = otherMonth->monthNumber;
        else if (month.value() != otherMonth->monthNumber) [[unlikely]] {
            throwRangeError(globalObject, scope, "month and monthCode properties must match if both are provided"_s);
            return { };
        }
    }

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

    // Duplicate code from TemporalPlainDate::toPlainDate so we can convert from
    // double to int32_t / unsigned here
    if (year && !ISO8601::isYearWithinLimits(*year)) [[unlikely]] {
        throwRangeError(globalObject, scope, "year is out of range"_s);
        return { };
    }

    int32_t yearToUse = defaultYear;
    if (year)
        yearToUse = static_cast<int32_t>(*year);
    unsigned monthToUse = defaultMonth;
    if (month) {
        if (overflow == TemporalOverflow::Constrain)
            monthToUse = std::min<unsigned>(*month, 12);
        else {
            if (!(month >= 1 && month <= 12)) [[unlikely]] {
                throwRangeError(globalObject, scope, "month is out of range"_s);
                return { };
            }
            monthToUse = *month;
        }
    }
    uint8_t daysInMonth = ISO8601::daysInMonth(yearToUse, monthToUse);

    unsigned dayToUse = day.value_or(defaultDay);

    if (overflow == TemporalOverflow::Constrain)
        dayToUse = std::min<unsigned>(dayToUse, daysInMonth);
    else if (!(dayToUse >= 1 && dayToUse <= daysInMonth)) [[unlikely]] {
        throwRangeError(globalObject, scope, "day is out of range"_s);
        return { };
    }

    return { yearToUse, monthToUse, dayToUse, otherMonth, overflow, any };
}

std::optional<int32_t> TemporalPlainDate::toDay(JSGlobalObject* globalObject, JSObject* temporalDateLike)
{
    VM& vm = globalObject->vm();
    auto scope = DECLARE_THROW_SCOPE(vm);

    std::optional<int32_t> day;
    JSValue dayProperty = temporalDateLike->get(globalObject, vm.propertyNames->day);
    RETURN_IF_EXCEPTION(scope, { });
    if (!dayProperty.isUndefined()) {
        double doubleDay = dayProperty.toIntegerOrInfinity(globalObject);
        RETURN_IF_EXCEPTION(scope, { });

        if (!std::isfinite(doubleDay)) [[unlikely]] {
            throwRangeError(globalObject, scope, "day property must be finite"_s);
            return { };
        }

        if (!isInBounds<int32_t>(doubleDay))[[unlikely]] {
            // Later checks will report error
            day = ISO8601::outOfRangeYear;
        } else
            day = static_cast<int32_t>(doubleDay);
    }
    return day;
}

std::optional<int32_t> TemporalPlainDate::toYear(JSGlobalObject* globalObject, JSObject* temporalDateLike)
{
    VM& vm = globalObject->vm();
    auto scope = DECLARE_THROW_SCOPE(vm);

    std::optional<int32_t> year;
    JSValue yearProperty = temporalDateLike->get(globalObject, vm.propertyNames->year);
    RETURN_IF_EXCEPTION(scope, { });
    if (!yearProperty.isUndefined()) {
        double doubleYear = yearProperty.toIntegerOrInfinity(globalObject);
        RETURN_IF_EXCEPTION(scope, { });

        if (!std::isfinite(doubleYear)) [[unlikely]] {
            throwRangeError(globalObject, scope, "year property must be finite"_s);
            return { };
        }

        if (!ISO8601::isYearWithinLimits(doubleYear)) [[unlikely]]
            year = ISO8601::outOfRangeYear;
        else
            year = static_cast<int32_t>(doubleYear);
    }
    return year;
}

std::tuple<std::optional<int32_t>, std::optional<ParsedMonthCode>, std::optional<int32_t>>
TemporalPlainDate::toYearMonth(JSGlobalObject* globalObject, JSObject* temporalDateLike)
{
    VM& vm = globalObject->vm();
    auto scope = DECLARE_THROW_SCOPE(vm);

    std::optional<int32_t> month;
    JSValue monthProperty = temporalDateLike->get(globalObject, vm.propertyNames->month);
    RETURN_IF_EXCEPTION(scope, { });
    if (!monthProperty.isUndefined()) {
        double doubleMonth = monthProperty.toIntegerOrInfinity(globalObject);
        RETURN_IF_EXCEPTION(scope, { });

        if (!std::isfinite(doubleMonth)) [[unlikely]] {
            throwRangeError(globalObject, scope, "month property must be finite"_s);
            return { };
        }

        // See step 9(c)(iv) of PrepareCalendarFields
        // https://tc39.es/proposal-temporal/#sec-temporal-preparecalendarfields
        if (doubleMonth <= 0) {
            throwRangeError(globalObject, scope, "month property must be a positive integer"_s);
            return { };
        }

        if (!isInBounds<int32_t>(doubleMonth)) [[unlikely]] {
            // Later checks will report error
            month = ISO8601::outOfRangeYear;
        } else
            month = static_cast<int32_t>(doubleMonth);
    }

    std::optional<ParsedMonthCode> monthCode;
    JSValue monthCodeProperty = temporalDateLike->get(globalObject, vm.propertyNames->monthCode);
    RETURN_IF_EXCEPTION(scope, { });
    if (!monthCodeProperty.isUndefined()) {
        auto monthCodeString = monthCodeProperty.toWTFString(globalObject);
        RETURN_IF_EXCEPTION(scope, { });

        monthCode = ISO8601::parseMonthCode(monthCodeString);
    }

    scope.release();
    auto year = toYear(globalObject, temporalDateLike);
    RETURN_IF_EXCEPTION(scope, { });

    return { month, monthCode, year };
}

ISO8601::PlainDate TemporalPlainDate::with(JSGlobalObject* globalObject, JSObject* temporalDateLike, JSValue optionsValue)
{
    VM& vm = globalObject->vm();
    auto scope = DECLARE_THROW_SCOPE(vm);

    rejectObjectWithCalendarOrTimeZone(globalObject, temporalDateLike);
    RETURN_IF_EXCEPTION(scope, { });

    if (!calendar()->isISO8601()) [[unlikely]] {
        throwRangeError(globalObject, scope, "unimplemented: with non-ISO8601 calendar"_s);
        return { };
    }

    auto [y, m, d, optionalMonthCode, overflow, any] = mergeDateFields(globalObject, temporalDateLike, optionsValue, year(), month(), day());
    RETURN_IF_EXCEPTION(scope, { });
    if (any == TemporalAnyProperties::None) [[unlikely]] {
        throwTypeError(globalObject, scope, "Object must contain at least one Temporal date property"_s);
        return { };
    }

    RELEASE_AND_RETURN(scope, TemporalCalendar::isoDateFromFields(globalObject, TemporalDateFormat::Date,
        y, m, d, optionalMonthCode, overflow));
}

// https://tc39.es/proposal-temporal/#sec-getutcepochnanoseconds
static Int128 getUTCEpochNanoseconds(ISO8601::PlainDate isoDate)
{
    return getUTCEpochNanoseconds(
        std::tuple<ISO8601::PlainDate, ISO8601::PlainTime>(
            isoDate, ISO8601::PlainTime()));
}

ISO8601::Duration TemporalPlainDate::differenceTemporalPlainDate(JSGlobalObject* globalObject, DifferenceOperation op, TemporalPlainDate* other, TemporalUnit smallestUnit, TemporalUnit largestUnit, RoundingMode roundingMode, double increment)
{
    VM& vm = globalObject->vm();
    auto scope = DECLARE_THROW_SCOPE(vm);

    if (!TemporalCalendar::isoDateCompare(plainDate(), other->plainDate()))
        return ISO8601::Duration();
    std::optional<ISO8601::Duration> dateDifference = TemporalCalendar::calendarDateUntil(plainDate(), other->plainDate(), largestUnit);
    // Both dates are within range, so the difference should be a valid Duration.
    ASSERT(dateDifference);
    ISO8601::InternalDuration duration = ISO8601::InternalDuration::combineDateAndTimeDuration(dateDifference.value(), 0);
    if (smallestUnit != TemporalUnit::Day || increment != 1) {
        auto isoDate = plainDate();
        auto isoDateOther = other->plainDate();
        Int128 destEpochNs = getUTCEpochNanoseconds(isoDateOther);
        TemporalDuration::roundRelativeDuration(
            globalObject, duration, destEpochNs, isoDate, largestUnit,
            increment, smallestUnit, roundingMode);
        RETURN_IF_EXCEPTION(scope, { });
    }
    auto result = TemporalDuration::temporalDurationFromInternal(duration, TemporalUnit::Day);
    if (op == DifferenceOperation::Since)
        result = -result;
    return result;
}

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

    bool calendarsMatch = calendar()->equals(globalObject, other->calendar());
    RETURN_IF_EXCEPTION(scope, { });
    if (!calendarsMatch) {
        throwRangeError(globalObject, scope, "calendars must match"_s);
        return { };
    }

    if (!calendar()->isISO8601()) {
        throwRangeError(globalObject, scope, "unimplemented: with non-ISO8601 calendar"_s);
        return { };
    }

    auto [smallestUnit, largestUnit, roundingMode, increment] = extractDifferenceOptions(globalObject, optionsValue, UnitGroup::Date, TemporalUnit::Day, TemporalUnit::Day);
    RETURN_IF_EXCEPTION(scope, { });

    RELEASE_AND_RETURN(scope, differenceTemporalPlainDate(globalObject,
        DifferenceOperation::Until, other, smallestUnit, largestUnit, roundingMode, increment));
}

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

    bool calendarsMatch = calendar()->equals(globalObject, other->calendar());
    RETURN_IF_EXCEPTION(scope, { });
    if (!calendarsMatch) {
        throwRangeError(globalObject, scope, "calendars must match"_s);
        return { };
    }

    if (!calendar()->isISO8601()) {
        throwRangeError(globalObject, scope, "unimplemented: with non-ISO8601 calendar"_s);
        return { };
    }

    auto [smallestUnit, largestUnit, roundingMode, increment] = extractDifferenceOptions(globalObject, optionsValue, UnitGroup::Date, TemporalUnit::Day, TemporalUnit::Day);
    RETURN_IF_EXCEPTION(scope, { });
    roundingMode = negateTemporalRoundingMode(roundingMode);

    RELEASE_AND_RETURN(scope, differenceTemporalPlainDate(globalObject,
        DifferenceOperation::Since, other, smallestUnit, largestUnit, roundingMode, increment));
}

String TemporalPlainDate::monthCode() const
{
    return ISO8601::monthCode(m_plainDate.month());
}

uint8_t TemporalPlainDate::dayOfWeek() const
{
    return ISO8601::dayOfWeek(m_plainDate);
}

uint16_t TemporalPlainDate::dayOfYear() const
{
    return ISO8601::dayOfYear(m_plainDate);
}

uint8_t TemporalPlainDate::weekOfYear() const
{
    return ISO8601::weekOfYear(m_plainDate);
}

} // namespace JSC
