/*
 * Copyright (C) 1999-2000 Harri Porten (porten@kde.org)
 * Copyright (C) 2006-2021 Apple Inc. All rights reserved.
 * Copyright (C) 2009 Google Inc. All rights reserved.
 * Copyright (C) 2007-2009 Torch Mobile, Inc.
 * Copyright (C) 2010 &yet, LLC. (nate@andyet.net)
 *
 * The Original Code is Mozilla Communicator client code, released
 * March 31, 1998.
 *
 * The Initial Developer of the Original Code is
 * Netscape Communications Corporation.
 * Portions created by the Initial Developer are Copyright (C) 1998
 * the Initial Developer. All Rights Reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 *
 * Alternatively, the contents of this file may be used under the terms
 * of either the Mozilla Public License Version 1.1, found at
 * http://www.mozilla.org/MPL/ (the "MPL") or the GNU General Public
 * License Version 2.0, found at http://www.fsf.org/copyleft/gpl.html
 * (the "GPL"), in which case the provisions of the MPL or the GPL are
 * applicable instead of those above.  If you wish to allow use of your
 * version of this file only under the terms of one of those two
 * licenses (the MPL or the GPL) and not to allow others to use your
 * version of this file under the LGPL, indicate your decision by
 * deletingthe provisions above and replace them with the notice and
 * other provisions required by the MPL or the GPL, as the case may be.
 * If you do not delete the provisions above, a recipient may use your
 * version of this file under any of the LGPL, the MPL or the GPL.

 * Copyright 2006-2008 the V8 project authors. All rights reserved.
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are
 * met:
 *
 *     * Redistributions of source code must retain the above copyright
 *       notice, this list of conditions and the following disclaimer.
 *     * 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.
 *     * Neither the name of Google Inc. nor the names of its
 *       contributors may be used to endorse or promote products derived
 *       from this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND 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 THE COPYRIGHT
 * OWNER OR 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 "JSDateMath.h"

#include "ExceptionHelpers.h"
#include "VM.h"
#include <limits>
#include <wtf/Language.h>
#include <wtf/unicode/icu/ICUHelpers.h>

#if U_ICU_VERSION_MAJOR_NUM >= 69 || (U_ICU_VERSION_MAJOR_NUM == 68 && USE(APPLE_INTERNAL_SDK))
#define HAVE_ICU_C_TIMEZONE_API 1
#ifdef U_HIDE_DRAFT_API
#undef U_HIDE_DRAFT_API
#endif
#include <unicode/ucal.h>
#else
// icu::TimeZone and icu::BasicTimeZone features are only available in ICU C++ APIs.
// We use these C++ APIs as an exception.
#undef U_SHOW_CPLUSPLUS_API
#define U_SHOW_CPLUSPLUS_API 1
#include <unicode/basictz.h>
#include <unicode/locid.h>
#include <unicode/timezone.h>
#include <unicode/unistr.h>
#undef U_SHOW_CPLUSPLUS_API
#define U_SHOW_CPLUSPLUS_API 0
#endif

namespace JSC {

#if PLATFORM(COCOA)
std::atomic<uint64_t> lastTimeZoneID { 1 };
#endif

#if HAVE(ICU_C_TIMEZONE_API)
class OpaqueICUTimeZone {
    WTF_MAKE_FAST_ALLOCATED(OpaqueICUTimeZone);
public:
    std::unique_ptr<UCalendar, ICUDeleter<ucal_close>> m_calendar;
    String m_canonicalTimeZoneID;
};
#else
static icu::TimeZone* toICUTimeZone(OpaqueICUTimeZone* timeZone)
{
    return bitwise_cast<icu::TimeZone*>(timeZone);
}
static OpaqueICUTimeZone* toOpaqueICUTimeZone(icu::TimeZone* timeZone)
{
    return bitwise_cast<OpaqueICUTimeZone*>(timeZone);
}
#endif

void OpaqueICUTimeZoneDeleter::operator()(OpaqueICUTimeZone* timeZone)
{
    if (timeZone) {
#if HAVE(ICU_C_TIMEZONE_API)
        delete timeZone;
#else
        delete toICUTimeZone(timeZone);
#endif
    }
}

// Get the combined UTC + DST offset for the time passed in.
//
// NOTE: The implementation relies on the fact that no time zones have
// more than one daylight savings offset change per month.
// If this function is called with NaN it returns random value.
LocalTimeOffset DateCache::calculateLocalTimeOffset(double millisecondsFromEpoch, WTF::TimeType inputTimeType)
{
    int32_t rawOffset = 0;
    int32_t dstOffset = 0;
    UErrorCode status = U_ZERO_ERROR;

    // This function can fail input date is invalid: NaN etc.
    // We can return any values in this case since later we fail when computing non timezone offset part anyway.
    constexpr LocalTimeOffset failed { false, 0 };

#if HAVE(ICU_C_TIMEZONE_API)
    auto& timeZoneCache = *this->timeZoneCache();
    ucal_setMillis(timeZoneCache.m_calendar.get(), millisecondsFromEpoch, &status);
    if (U_FAILURE(status))
        return failed;

    if (inputTimeType != WTF::LocalTime) {
        rawOffset = ucal_get(timeZoneCache.m_calendar.get(), UCAL_ZONE_OFFSET, &status);
        if (U_FAILURE(status))
            return failed;
        dstOffset = ucal_get(timeZoneCache.m_calendar.get(), UCAL_DST_OFFSET, &status);
        if (U_FAILURE(status))
            return failed;
    } else {
        ucal_getTimeZoneOffsetFromLocal(timeZoneCache.m_calendar.get(), UCAL_TZ_LOCAL_FORMER, UCAL_TZ_LOCAL_FORMER, &rawOffset, &dstOffset, &status);
        if (U_FAILURE(status))
            return failed;
    }
#else
    auto& timeZoneCache = *toICUTimeZone(this->timeZoneCache());
    if (inputTimeType != WTF::LocalTime) {
        constexpr bool isLocalTime = false;
        timeZoneCache.getOffset(millisecondsFromEpoch, isLocalTime, rawOffset, dstOffset, status);
        if (U_FAILURE(status))
            return failed;
    } else {
        // icu::TimeZone is a timezone instance which inherits icu::BasicTimeZone.
        // https://unicode-org.atlassian.net/browse/ICU-13705 will move getOffsetFromLocal to icu::TimeZone.
        static_cast<const icu::BasicTimeZone&>(timeZoneCache).getOffsetFromLocal(millisecondsFromEpoch, icu::BasicTimeZone::kFormer, icu::BasicTimeZone::kFormer, rawOffset, dstOffset, status);
        if (U_FAILURE(status))
            return failed;
    }
#endif

    return { !!dstOffset, rawOffset + dstOffset };
}

LocalTimeOffset DateCache::localTimeOffset(double millisecondsFromEpoch, WTF::TimeType inputTimeType)
{
    LocalTimeOffsetCache& cache = inputTimeType == WTF::LocalTime ? m_localTimeOffsetCache : m_utcTimeOffsetCache;

    double start = cache.start;
    double end = cache.end;

    auto resetCache = [&]() {
        // Compute the DST offset for the time and shrink the cache interval
        // to only contain the time. This allows fast repeated DST offset
        // computations for the same time.
        LocalTimeOffset offset = calculateLocalTimeOffset(millisecondsFromEpoch, inputTimeType);
        cache.offset = offset;
        cache.start = millisecondsFromEpoch;
        cache.end = millisecondsFromEpoch;
        cache.incrementStart = WTF::msPerMonth;
        cache.incrementEnd = WTF::msPerMonth;
        return offset;
    };

    // If the time fits in the cached interval, return the cached offset.
    if (start <= millisecondsFromEpoch && millisecondsFromEpoch <= end)
        return cache.offset;

    if (start <= millisecondsFromEpoch) {
        // Compute a possible new interval end.
        double newEnd = end + cache.incrementEnd;
        if (!(millisecondsFromEpoch <= newEnd))
            return resetCache();

        LocalTimeOffset endOffset = calculateLocalTimeOffset(newEnd, inputTimeType);
        if (cache.offset == endOffset) {
            // If the offset at the end of the new interval still matches
            // the offset in the cache, we grow the cached time interval
            // and return the offset.
            cache.end = newEnd;
            cache.incrementStart = WTF::msPerMonth;
            cache.incrementEnd = WTF::msPerMonth;
            return endOffset;
        }
        LocalTimeOffset offset = calculateLocalTimeOffset(millisecondsFromEpoch, inputTimeType);
        if (offset == endOffset) {
            // The offset at the given time is equal to the offset at the
            // new end of the interval, so that means that we've just skipped
            // the point in time where the DST offset change occurred. Update
            // the interval to reflect this and reset the increment.
            cache.start = millisecondsFromEpoch;
            cache.end = newEnd;
            cache.incrementStart = WTF::msPerMonth;
            cache.incrementEnd = WTF::msPerMonth;
        } else {
            // The interval contains a DST offset change and the given time is
            // before it. Adjust the increment to avoid a linear search for
            // the offset change point and change the end of the interval.
            cache.incrementEnd /= 3;
            cache.end = millisecondsFromEpoch;
        }
        // Update the offset in the cache and return it.
        cache.offset = offset;
        return offset;
    }

    // Compute a possible new interval start.
    double newStart = start - cache.incrementStart;
    if (!(newStart <= millisecondsFromEpoch))
        return resetCache();

    LocalTimeOffset startOffset = calculateLocalTimeOffset(newStart, inputTimeType);
    if (cache.offset == startOffset) {
        // If the offset at the start of the new interval still matches
        // the offset in the cache, we grow the cached time interval
        // and return the offset.
        cache.start = newStart;
        cache.incrementStart = WTF::msPerMonth;
        cache.incrementEnd = WTF::msPerMonth;
        return startOffset;
    }
    LocalTimeOffset offset = calculateLocalTimeOffset(millisecondsFromEpoch, inputTimeType);
    if (offset == startOffset) {
        // The offset at the given time is equal to the offset at the
        // new start of the interval, so that means that we've just skipped
        // the point in time where the DST offset change occurred. Update
        // the interval to reflect this and reset the increment.
        cache.start = newStart;
        cache.end = millisecondsFromEpoch;
        cache.incrementStart = WTF::msPerMonth;
        cache.incrementEnd = WTF::msPerMonth;
    } else {
        // The interval contains a DST offset change and the given time is
        // before it. Adjust the increment to avoid a linear search for
        // the offset change point and change the end of the interval.
        cache.incrementStart /= 3;
        cache.start = millisecondsFromEpoch;
    }
    // Update the offset in the cache and return it.
    cache.offset = offset;
    return offset;
}

double DateCache::gregorianDateTimeToMS(const GregorianDateTime& t, double milliseconds, WTF::TimeType inputTimeType)
{
    double day = dateToDaysFrom1970(t.year(), t.month(), t.monthDay());
    double ms = timeToMS(t.hour(), t.minute(), t.second(), milliseconds);
    double localTimeResult = (day * WTF::msPerDay) + ms;

    double localToUTCTimeOffset = inputTimeType == WTF::LocalTime ? localTimeOffset(localTimeResult, inputTimeType).offset : 0;

    return localTimeResult - localToUTCTimeOffset;
}

// input is UTC
void DateCache::msToGregorianDateTime(double millisecondsFromEpoch, WTF::TimeType outputTimeType, GregorianDateTime& tm)
{
    LocalTimeOffset localTime;
    if (outputTimeType == WTF::LocalTime) {
        localTime = localTimeOffset(millisecondsFromEpoch);
        millisecondsFromEpoch += localTime.offset;
    }
    tm = GregorianDateTime(millisecondsFromEpoch, localTime);
}

double DateCache::parseDate(JSGlobalObject* globalObject, VM& vm, const String& date)
{
    auto scope = DECLARE_THROW_SCOPE(vm);

    if (date == m_cachedDateString)
        return m_cachedDateStringValue;
    auto expectedString = date.tryGetUtf8();
    if (!expectedString) {
        if (expectedString.error() == UTF8ConversionError::OutOfMemory)
            throwOutOfMemoryError(globalObject, scope);
        // https://tc39.github.io/ecma262/#sec-date-objects section 20.3.3.2 states that:
        // "Unrecognizable Strings or dates containing illegal element values in the
        // format String shall cause Date.parse to return NaN."
        return std::numeric_limits<double>::quiet_NaN();
    }

    auto parseDateImpl = [this] (const char* dateString) {
        bool isLocalTime;
        double value = WTF::parseES5DateFromNullTerminatedCharacters(dateString, isLocalTime);
        if (std::isnan(value))
            value = WTF::parseDateFromNullTerminatedCharacters(dateString, isLocalTime);

        if (isLocalTime)
            value -= localTimeOffset(value, WTF::LocalTime).offset;

        return value;
    };

    auto dateUtf8 = expectedString.value();
    double value = parseDateImpl(dateUtf8.data());
    m_cachedDateString = date;
    m_cachedDateStringValue = value;
    return value;
}

// https://tc39.es/ecma402/#sec-defaulttimezone
String DateCache::defaultTimeZone()
{
#if HAVE(ICU_C_TIMEZONE_API)
    return timeZoneCache()->m_canonicalTimeZoneID;
#else
    icu::UnicodeString timeZoneID;
    icu::UnicodeString canonicalTimeZoneID;
    auto& timeZone = *toICUTimeZone(timeZoneCache());
    timeZone.getID(timeZoneID);

    UErrorCode status = U_ZERO_ERROR;
    UBool isSystem = false;
    icu::TimeZone::getCanonicalID(timeZoneID, canonicalTimeZoneID, isSystem, status);
    if (U_FAILURE(status))
        return "UTC"_s;

    String canonical = String(canonicalTimeZoneID.getBuffer(), canonicalTimeZoneID.length());
    if (isUTCEquivalent(canonical))
        return "UTC"_s;

    return canonical;
#endif
}

String DateCache::timeZoneDisplayName(bool isDST)
{
    if (m_timeZoneStandardDisplayNameCache.isNull()) {
#if HAVE(ICU_C_TIMEZONE_API)
        auto& timeZoneCache = *this->timeZoneCache();
        CString language = defaultLanguage().utf8();
        {
            Vector<UChar, 32> standardDisplayNameBuffer;
            auto status = callBufferProducingFunction(ucal_getTimeZoneDisplayName, timeZoneCache.m_calendar.get(), UCAL_STANDARD, language.data(), standardDisplayNameBuffer);
            if (U_SUCCESS(status))
                m_timeZoneStandardDisplayNameCache = String::adopt(WTFMove(standardDisplayNameBuffer));
        }
        {
            Vector<UChar, 32> dstDisplayNameBuffer;
            auto status = callBufferProducingFunction(ucal_getTimeZoneDisplayName, timeZoneCache.m_calendar.get(), UCAL_DST, language.data(), dstDisplayNameBuffer);
            if (U_SUCCESS(status))
                m_timeZoneDSTDisplayNameCache = String::adopt(WTFMove(dstDisplayNameBuffer));
        }
#else
        auto& timeZoneCache = *toICUTimeZone(this->timeZoneCache());
        String language = defaultLanguage();
        icu::Locale locale(language.utf8().data());
        {
            icu::UnicodeString standardDisplayName;
            timeZoneCache.getDisplayName(false /* inDaylight */, icu::TimeZone::LONG, locale, standardDisplayName);
            m_timeZoneStandardDisplayNameCache = String(standardDisplayName.getBuffer(), standardDisplayName.length());
        }
        {
            icu::UnicodeString dstDisplayName;
            timeZoneCache.getDisplayName(true /* inDaylight */, icu::TimeZone::LONG, locale, dstDisplayName);
            m_timeZoneDSTDisplayNameCache = String(dstDisplayName.getBuffer(), dstDisplayName.length());
        }
#endif
    }
    if (isDST)
        return m_timeZoneDSTDisplayNameCache;
    return m_timeZoneStandardDisplayNameCache;
}

#if PLATFORM(COCOA)
static void timeZoneChangeNotification(CFNotificationCenterRef, void*, CFStringRef, const void*, CFDictionaryRef)
{
    ASSERT(isMainThread());
    ++lastTimeZoneID;
}
#endif

// To confine icu::TimeZone destructor invocation in this file.
DateCache::DateCache()
{
#if PLATFORM(COCOA)
    static std::once_flag onceKey;
    std::call_once(onceKey, [&] {
        CFNotificationCenterAddObserver(CFNotificationCenterGetLocalCenter(), nullptr, timeZoneChangeNotification, kCFTimeZoneSystemTimeZoneDidChangeNotification, nullptr, CFNotificationSuspensionBehaviorDeliverImmediately);
    });
#endif
}

DateCache::~DateCache() = default;

Ref<DateInstanceData> DateCache::cachedDateInstanceData(double millisecondsFromEpoch)
{
    return *m_dateInstanceCache.add(millisecondsFromEpoch);
}

void DateCache::timeZoneCacheSlow()
{
    ASSERT(!m_timeZoneCache);

    Vector<UChar, 32> timeZoneID;
    getTimeZoneOverride(timeZoneID);
#if HAVE(ICU_C_TIMEZONE_API)
    auto* cache = new OpaqueICUTimeZone;

    String canonical;
    UErrorCode status = U_ZERO_ERROR;
    if (timeZoneID.isEmpty()) {
        status = callBufferProducingFunction(ucal_getHostTimeZone, timeZoneID);
        ASSERT_UNUSED(status, U_SUCCESS(status));
    }
    if (U_SUCCESS(status)) {
        Vector<UChar, 32> canonicalBuffer;
        auto status = callBufferProducingFunction(ucal_getCanonicalTimeZoneID, timeZoneID.data(), timeZoneID.size(), canonicalBuffer, nullptr);
        if (U_SUCCESS(status))
            canonical = String(canonicalBuffer);
    }
    if (canonical.isNull() || isUTCEquivalent(canonical))
        canonical = "UTC"_s;
    cache->m_canonicalTimeZoneID = WTFMove(canonical);

    status = U_ZERO_ERROR;
    cache->m_calendar = std::unique_ptr<UCalendar, ICUDeleter<ucal_close>>(ucal_open(timeZoneID.data(), timeZoneID.size(), "", UCAL_DEFAULT, &status));
    ASSERT_UNUSED(status, U_SUCCESS(status));
    ucal_setGregorianChange(cache->m_calendar.get(), minECMAScriptTime, &status); // Ignore "unsupported" error.
    m_timeZoneCache = std::unique_ptr<OpaqueICUTimeZone, OpaqueICUTimeZoneDeleter>(cache);
#else
    if (!timeZoneID.isEmpty()) {
        m_timeZoneCache = std::unique_ptr<OpaqueICUTimeZone, OpaqueICUTimeZoneDeleter>(toOpaqueICUTimeZone(icu::TimeZone::createTimeZone(icu::UnicodeString(timeZoneID.data(), timeZoneID.size()))));
        return;
    }
    // Do not use icu::TimeZone::createDefault. ICU internally has a cache for timezone and createDefault returns this cached value.
    m_timeZoneCache = std::unique_ptr<OpaqueICUTimeZone, OpaqueICUTimeZoneDeleter>(toOpaqueICUTimeZone(icu::TimeZone::detectHostTimeZone()));
#endif
}

void DateCache::resetIfNecessarySlow()
{
    // FIXME: We should clear it only when we know the timezone has been changed on Non-Cocoa platforms.
    // https://bugs.webkit.org/show_bug.cgi?id=218365
    m_timeZoneCache.reset();
    m_utcTimeOffsetCache = LocalTimeOffsetCache();
    m_localTimeOffsetCache = LocalTimeOffsetCache();
    m_cachedDateString = String();
    m_cachedDateStringValue = std::numeric_limits<double>::quiet_NaN();
    m_dateInstanceCache.reset();
    m_timeZoneStandardDisplayNameCache = String();
    m_timeZoneDSTDisplayNameCache = String();
}

} // namespace JSC
