/*
 * Copyright (C) 2021 Igalia S.L.
 * 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 "TemporalInstant.h"

#include "AuxiliaryBarrierInlines.h"
#include "Error.h"
#include "ISO8601.h"
#include "IntlObjectInlines.h"
#include "JSBigInt.h"
#include "JSGlobalObject.h"
#include "JSObjectInlines.h"
#include "MathCommon.h"
#include "StructureInlines.h"
#include "TemporalDuration.h"
#include "TemporalObject.h"
#include "TemporalTimeZone.h"
#include <wtf/text/MakeString.h>

namespace JSC {

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

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

TemporalInstant::TemporalInstant(VM& vm, Structure* structure, ISO8601::ExactTime exactTime)
    : Base(vm, structure)
    , m_exactTime(exactTime)
{
}

TemporalInstant* TemporalInstant::create(VM& vm, Structure* structure, ISO8601::ExactTime exactTime)
{
    ASSERT(exactTime.isValid());
    auto* object = new (NotNull, allocateCell<TemporalInstant>(vm)) TemporalInstant(vm, structure, exactTime);
    object->finishCreation(vm);
    return object;
}

// CreateTemporalInstant ( epochNanoseconds [ , newTarget ] )
// https://tc39.es/proposal-temporal/#sec-temporal-createtemporalinstant
TemporalInstant* TemporalInstant::tryCreateIfValid(JSGlobalObject* globalObject, ISO8601::ExactTime exactTime, Structure* structure)
{
    VM& vm = globalObject->vm();
    auto scope = DECLARE_THROW_SCOPE(vm);

    if (!exactTime.isValid()) {
        throwRangeError(globalObject, scope, makeString(exactTime.asString(), " epoch nanoseconds is outside of supported range for Temporal.Instant"_s));
        return nullptr;
    }

    return create(vm, structure ? structure : globalObject->instantStructure(), exactTime);
}

TemporalInstant* TemporalInstant::tryCreateIfValid(JSGlobalObject* globalObject, JSValue value, Structure* structure)
{
    VM& vm = globalObject->vm();
    auto scope = DECLARE_THROW_SCOPE(vm);

    JSValue epochNanoseconds = value.toBigInt(globalObject);
    RETURN_IF_EXCEPTION(scope, nullptr);

#if USE(BIGINT32)
    if (epochNanoseconds.isBigInt32()) {
        int32_t total = epochNanoseconds.bigInt32AsInt32();
        ISO8601::ExactTime exactTime { total };
        ASSERT(exactTime.isValid());
        return create(vm, structure ? structure : globalObject->instantStructure(), exactTime);
    }
#endif

    JSBigInt* bigint = asHeapBigInt(epochNanoseconds);
    bool bigIntTooLong = false;
    Int128 total;
    if constexpr (sizeof(JSBigInt::Digit) == 4) {
        Int128 d0 { bigint->length() > 0 ? bigint->digit(0) : 0 };
        Int128 d1 { bigint->length() > 1 ? bigint->digit(1) : 0 };
        Int128 d2 { bigint->length() > 2 ? bigint->digit(2) : 0 };
        total = d2 << 64 | d1 << 32 | d0;
        bigIntTooLong = bigint->length() > 3;
    } else {
        ASSERT(sizeof(JSBigInt::Digit) == 8);
        // Handle maxint128 < abs(bigint) <= maxuint128 explicitly, otherwise
        // the int128 arithmetic below is undefined
        if (bigint->length() > 1 && (bigint->digit(1) & 0x8000'0000'0000'0000)) {
            total = 0;
            bigIntTooLong = true;
        } else {
            Int128 low { bigint->length() > 0 ? bigint->digit(0) : 0 };
            Int128 high { bigint->length() > 1 ? bigint->digit(1) : 0 };
            total = high << 64 | low;
            bigIntTooLong = bigint->length() > 2;
        }
    }
    ISO8601::ExactTime exactTime { total * (bigint->sign() ? -1 : 1) };

    if (bigIntTooLong || !exactTime.isValid()) {
        String argAsString = bigint->toString(globalObject, 10);
        if (scope.exception()) {
            scope.clearException();
            argAsString = "The given number of"_s;
        }

        throwRangeError(globalObject, scope, makeString(ellipsizeAt(100, argAsString), " epoch nanoseconds is outside of the supported range for Temporal.Instant"_s));
        return nullptr;
    }

    return create(vm, structure ? structure : globalObject->instantStructure(), exactTime);
}

// ToTemporalInstant ( item )
// https://tc39.es/proposal-temporal/#sec-temporal-totemporalinstant
TemporalInstant* TemporalInstant::toInstant(JSGlobalObject* globalObject, JSValue itemValue)
{
    VM& vm = globalObject->vm();
    auto scope = DECLARE_THROW_SCOPE(vm);

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

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

    // FIXME: when Temporal.ZonedDateTime lands
    // if (itemValue.inherits<TemporalZonedDateTime>())
    //    return TemporalInstant::create(vm, globalObject->instantStructure(), jsCast<TemporalZonedDateTime*>(itemValue)->epochTime());

    String string = itemValue.toWTFString(globalObject);
    RETURN_IF_EXCEPTION(scope, nullptr);

    auto parsedExactTime = ISO8601::parseInstant(string);
    if (!parsedExactTime) {
        throwRangeError(globalObject, scope, makeString("'"_s, ellipsizeAt(100, string), "' is not a valid Temporal.Instant string"_s));
        return nullptr;
    }

    RELEASE_AND_RETURN(scope, tryCreateIfValid(globalObject, parsedExactTime.value()));
}

// Temporal.Instant.from ( item )
// https://tc39.es/proposal-temporal/#sec-temporal.instant.from
TemporalInstant* TemporalInstant::from(JSGlobalObject* globalObject, JSValue itemValue)
{
    VM& vm = globalObject->vm();

    if (itemValue.inherits<TemporalInstant>()) {
        ISO8601::ExactTime exactTime = jsCast<TemporalInstant*>(itemValue)->exactTime();
        return TemporalInstant::create(vm, globalObject->instantStructure(), exactTime);
    }

    return toInstant(globalObject, itemValue);
}

// Temporal.Instant.fromEpochMilliseconds ( epochMilliseconds )
// https://tc39.es/proposal-temporal/#sec-temporal.instant.fromepochmilliseconds
TemporalInstant* TemporalInstant::fromEpochMilliseconds(JSGlobalObject* globalObject, JSValue epochMillisecondsValue)
{
    VM& vm = globalObject->vm();
    auto scope = DECLARE_THROW_SCOPE(vm);

    double epochMilliseconds = epochMillisecondsValue.toNumber(globalObject);
    RETURN_IF_EXCEPTION(scope, nullptr);

    // NumberToBigInt step 1
    if (!isInteger(epochMilliseconds)) {
        throwRangeError(globalObject, scope, makeString(epochMilliseconds, " is not a valid integer number of epoch milliseconds"_s));
        return nullptr;
    }

    ISO8601::ExactTime exactTime = ISO8601::ExactTime::fromEpochMilliseconds(epochMilliseconds);
    RELEASE_AND_RETURN(scope, tryCreateIfValid(globalObject, exactTime));
}

// Temporal.Instant.fromEpochNanoseconds ( epochNanoseconds )
// https://tc39.es/proposal-temporal/#sec-temporal.instant.fromepochnanoseconds
TemporalInstant* TemporalInstant::fromEpochNanoseconds(JSGlobalObject* globalObject, JSValue epochNanosecondsValue)
{
    return tryCreateIfValid(globalObject, epochNanosecondsValue);
}

// Temporal.Instant.compare ( one, two )
// https://tc39.es/proposal-temporal/#sec-temporal.instant.compare
JSValue TemporalInstant::compare(JSGlobalObject* globalObject, JSValue oneValue, JSValue twoValue)
{
    VM& vm = globalObject->vm();
    auto scope = DECLARE_THROW_SCOPE(vm);

    TemporalInstant* one = toInstant(globalObject, oneValue);
    RETURN_IF_EXCEPTION(scope, { });

    TemporalInstant* two = toInstant(globalObject, twoValue);
    RETURN_IF_EXCEPTION(scope, { });

    if (one->exactTime() > two->exactTime())
        return jsNumber(1);
    if (one->exactTime() < two->exactTime())
        return jsNumber(-1);
    return jsNumber(0);
}

ISO8601::Duration TemporalInstant::difference(JSGlobalObject* globalObject, TemporalInstant* other, JSValue optionsValue) const
{
    // https://tc39.es/proposal-temporal/#sec-temporal.instant.prototype.since
    // https://tc39.es/proposal-temporal/#sec-temporal.instant.prototype.until
    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);

    TemporalUnit defaultLargestUnit = std::min(smallestUnit, TemporalUnit::Second);
    auto largest = temporalLargestUnit(globalObject, options, { TemporalUnit::Year, TemporalUnit::Month, TemporalUnit::Week, TemporalUnit::Day }, defaultLargestUnit);
    RETURN_IF_EXCEPTION(scope, { });
    TemporalUnit largestUnit = largest.value_or(defaultLargestUnit);

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

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

    std::optional<double> maxIncrement = maximumRoundingIncrement(smallestUnit);
    ASSERT(maxIncrement && *maxIncrement <= 1000); // unbounded increments are impossible with Temporal.Instant
    unsigned increment = temporalRoundingIncrement(globalObject, options, maxIncrement, false);
    RETURN_IF_EXCEPTION(scope, { });

    Int128 roundedDiff = exactTime().difference(other->exactTime(), increment, smallestUnit, roundingMode);
    // NOTE: Duration fields are currently doubles, and the total number of
    // nanoseconds may not fit in a double. This may need to change if the
    // internal representation of Duration changes.
    ASSERT(roundedDiff / 1'000'000'000 < INT64_MAX);
    double seconds { static_cast<double>(static_cast<int64_t>(roundedDiff / 1'000'000'000)) };
    double nanosecondsRemainder { static_cast<double>(static_cast<int64_t>(roundedDiff % 1'000'000'000)) };
    ISO8601::Duration result { 0, 0, 0, 0, 0, 0, seconds, 0, 0, nanosecondsRemainder };

    TemporalDuration::balance(result, largestUnit);
    return result;
}

static double maximumIncrement(TemporalUnit smallestUnit)
{
    switch (smallestUnit) {
    case TemporalUnit::Hour: return 24;
    case TemporalUnit::Minute: return 1440;
    case TemporalUnit::Second: return 86400;
    case TemporalUnit::Millisecond: return 8.64e7;
    case TemporalUnit::Microsecond: return 8.64e10;
    case TemporalUnit::Nanosecond: return 8.64e13;
    case TemporalUnit::Year:
    case TemporalUnit::Month:
    case TemporalUnit::Week:
    case TemporalUnit::Day:
    default:
        { }
    }
    RELEASE_ASSERT_NOT_REACHED();
}

ISO8601::ExactTime TemporalInstant::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();

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

    double increment = temporalRoundingIncrement(globalObject, options, maximumIncrement(smallestUnit), true);
    RETURN_IF_EXCEPTION(scope, { });

    return exactTime().round(increment, smallestUnit, roundingMode);
}

// Temporal.Instant.prototype.toString( [ options ] )
// https://tc39.es/proposal-temporal/#sec-temporal.instant.prototype.tostring
String TemporalInstant::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();

    JSObject* timeZone = nullptr;
    JSValue timeZoneValue = options->get(globalObject, vm.propertyNames->timeZone);
    RETURN_IF_EXCEPTION(scope, { });
    if (!timeZoneValue.isUndefined()) {
        timeZone = TemporalTimeZone::from(globalObject, timeZoneValue);
        RETURN_IF_EXCEPTION(scope, { });
    }

    PrecisionData data = secondsStringPrecision(globalObject, options);
    RETURN_IF_EXCEPTION(scope, { });

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

    ISO8601::ExactTime newExactTime { exactTime().round(data.increment, data.unit, roundingMode) };

    // FIXME: Missing, relies on TimeZone:
    // 1. Let roundedInstant be ! CreateTemporalInstant(roundedNs).
    // ...
    // 1. If _outputTimeZone_ is *undefined*, then
    //   1. Set _outputTimeZone_ to ! CreateTemporalTimeZone(*"UTC"*).
    // 1. Let _isoCalendar_ be ! GetISO8601Calendar().
    // 1. Let _dateTime_ be ? BuiltinTimeZoneGetPlainDateTimeFor(_outputTimeZone_, _instant_, _isoCalendar_).
    JSObject* outputTimeZone = timeZone;
    if (outputTimeZone) {
        throwVMError(globalObject, scope, "FIXME: Temporal.Instant.toString({timeZone}) not implemented yet"_s);
        return { };
    }

    return toString(newExactTime, timeZone, data);
}

// TemporalInstantToString ( instant, timeZone, precision )
// https://tc39.es/proposal-temporal/#sec-temporal-temporalinstanttostring
String TemporalInstant::toString(ISO8601::ExactTime exactTime, JSObject* timeZone, PrecisionData precision)
{
    GregorianDateTime gregorianDateTime { static_cast<double>(exactTime.epochMilliseconds()), LocalTimeOffset { } };
    StringBuilder builder;

    // If the year is outside the bounds of 0 and 9999 inclusive we want to
    // use the extended year format (PadISOYear).
    unsigned yearLength = 4;
    if (gregorianDateTime.year() > 9999 || gregorianDateTime.year() < 0) {
        builder.append(gregorianDateTime.year() < 0 ? '-' : '+');
        yearLength = 6;
    }

    builder.append(makeString(pad('0', yearLength, std::abs(gregorianDateTime.year())),
        '-', pad('0', 2, gregorianDateTime.month() + 1),
        '-', pad('0', 2, gregorianDateTime.monthDay()),
        'T', pad('0', 2, gregorianDateTime.hour()),
        ':', pad('0', 2, gregorianDateTime.minute())));

    static constexpr int nsPerSecond { 1'000'000'000 };
    int fraction { exactTime.nanosecondsFraction() };
    if (fraction < 0)
        fraction += nsPerSecond;

    formatSecondsStringPart(builder, gregorianDateTime.second(), fraction, precision);

    if (timeZone) {
        // FIXME: Missing, relies on TimeZone:
        //   1. Let _timeZoneString_ be ? BuiltinTimeZoneGetOffsetStringFor(_timeZone_, _instant_).
        builder.append('Z');
    } else
        builder.append('Z');

    return builder.toString();
}

} // namespace JSC
