//
// tzset.cpp
//
//      Copyright (c) Microsoft Corporation.  All rights reserved.
//
// Defines the _tzset() function which updates the global time zone state, and
// the _isindst() function, which tests whether a time is in Daylight Savings
// Time or not.
//
#include <corecrt_internal_time.h>
#include <locale.h>



_DEFINE_SET_FUNCTION(_set_daylight, int,  _daylight)
_DEFINE_SET_FUNCTION(_set_dstbias,  long, _dstbias )
_DEFINE_SET_FUNCTION(_set_timezone, long, _timezone)



// Pointer to a saved copy of the TZ value obtained in the previous call to the
// tzset functions, if one is available:
static wchar_t* last_wide_tz = nullptr;

// If the time zone was last updated by calling the system API, then the tz_info
// variable contains the time zone information and tz_api_used is set to true.
static int                   tz_api_used;
static TIME_ZONE_INFORMATION tz_info;

static __crt_state_management::dual_state_global<long> tzset_init_state;

namespace
{
    // Structure used to represent DST transition date/times:
    struct transitiondate
    {
        int  yr; // year of interest
        int  yd; // day of year
        int  ms; // milli-seconds in the day
    };

    enum class date_type
    {
        absolute_date,
        day_in_month
    };

    enum class transition_type
    {
        start_of_dst,
        end_of_dst
    };

    size_t const local_env_buffer_size = 256;
    int    const milliseconds_per_day  = 24 * 60 * 60 * 1000;
}

// DST start and end structures:
static transitiondate dststart = { -1, 0, 0 };
static transitiondate dstend   = { -1, 0, 0 };



//-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
//
// The _tzset() family of functions
//
//-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// Gets the value of the TZ environment variable.  If there is no TZ environment
// variable or if we do not have access to the environment, nullptr is returned.
// If the value of the TZ variable fits into the local_buffer, it is stored there
// and a pointer to the local_buffer is returned.  Otherwise, a buffer is
// dynamically allocated, the value is stored into that buffer, and a pointer to
// that buffer is returned.  In this case, the caller is responsible for freeing
// the buffer.
static wchar_t* get_tz_environment_variable(wchar_t (&local_buffer)[local_env_buffer_size]) throw()
{
    size_t required_length;
    errno_t const status = _wgetenv_s(&required_length, local_buffer, local_env_buffer_size, L"TZ");
    if (status == 0)
    {
        return local_buffer;
    }

    if (status != ERANGE)
    {
        return nullptr;
    }

    __crt_unique_heap_ptr<wchar_t> dynamic_buffer(_malloc_crt_t(wchar_t, required_length));
    if (dynamic_buffer.get() == nullptr)
    {
        return nullptr;
    }

    size_t actual_length;
    if (_wgetenv_s(&actual_length, dynamic_buffer.get(), required_length, L"TZ") != 0)
    {
        return nullptr;
    }

    return dynamic_buffer.detach();
}

static void __cdecl tzset_os_copy_to_tzname(const wchar_t * const timezone_name, wchar_t * const wide_tzname, char * const narrow_tzname, unsigned int const code_page)
{
    // Maximum time zone name from OS is 32 characters long
    // (see https://docs.microsoft.com/en-us/windows/desktop/api/timezoneapi/ns-timezoneapi-_time_zone_information)
    _ERRCHECK(wcsncpy_s(wide_tzname, _TZ_STRINGS_SIZE, timezone_name, 32));

    // Invalid characters are replaced by closest approximation or default character.
    // On other failure, leave narrow tzname blank.
    __acrt_WideCharToMultiByte(
        code_page,
        0,
        timezone_name,
        -1,
        narrow_tzname,
        _TZ_STRINGS_SIZE, // Passing -1 as source size, so null terminator included.
        nullptr,
        nullptr
    );
}

// Handles the _tzset if and only if there is no TZ environment variable.  In
// this case, we attempt to use the time zone information from the system.
static void __cdecl tzset_from_system_nolock() throw()
{
    _BEGIN_SECURE_CRT_DEPRECATION_DISABLE
    char** tzname = _tzname;
    wchar_t** wide_tzname = __wide_tzname();
    _END_SECURE_CRT_DEPRECATION_DISABLE

    long timezone = 0;
    int  daylight = 0;
    long dstbias  = 0;
    _ERRCHECK(_get_timezone(&timezone));
    _ERRCHECK(_get_daylight(&daylight));
    _ERRCHECK(_get_dstbias (&dstbias ));

    // If there is a last_wide_tz already, discard it:
    _free_crt(last_wide_tz);
    last_wide_tz = nullptr;

    if (GetTimeZoneInformation(&tz_info) != 0xFFFFFFFF)
    {
        // Record that the API was used:
        tz_api_used = 1;

        // Derive _timezone value from Bias and StandardBias fields.
        timezone = tz_info.Bias * 60;

        if (tz_info.StandardDate.wMonth != 0)
            timezone += tz_info.StandardBias * 60;

        // Check to see if there is a daylight time bias. Since the StandardBias
        // has been added into _timezone, it must be compensated for in the
        // value computed for _dstbias:
        if (tz_info.DaylightDate.wMonth != 0 && tz_info.DaylightBias != 0)
        {
            daylight = 1;
            dstbias = (tz_info.DaylightBias - tz_info.StandardBias) * 60;
        }
        else
        {
            daylight = 0;

            // Set the bias to 0 because GetTimeZoneInformation may return
            // TIME_ZONE_ID_DAYLIGHT even though there is no DST (e.g., in NT
            // 3.51, this can happen if automatic DST adjustment is disabled
            // in the Control Panel.
            dstbias = 0;
        }

        memset(wide_tzname[0], 0, _TZ_STRINGS_SIZE * sizeof(wchar_t));
        memset(wide_tzname[1], 0, _TZ_STRINGS_SIZE * sizeof(wchar_t));
        memset(tzname[0], 0, _TZ_STRINGS_SIZE);
        memset(tzname[1], 0, _TZ_STRINGS_SIZE);

        // Try to grab the name strings for both the time zone and the daylight
        // zone.  Note the wide character strings in tz_info must be converted
        // to multibyte character strings.  The locale code page must be used
        // for this.  Note that if setlocale() has not yet been called with
        // LC_ALL or LC_CTYPE, then the code page will be 0, which is CP_ACP,
        // so we will use the host's default ANSI code page.
        //
        // CRT_REFACTOR TODO We use the current locale for this transformation.
        // If per-thread locale has been enabled for this thread, then we'll be
        // using this thread's locale to update a global variable that is
        // accessed from multiple threads.  Does the time zone information also
        // need to be stored per-thread?
        unsigned const code_page = ___lc_codepage_func();

        tzset_os_copy_to_tzname(tz_info.StandardName, wide_tzname[0], tzname[0], code_page);
        tzset_os_copy_to_tzname(tz_info.DaylightName, wide_tzname[1], tzname[1], code_page);
    }

    _set_timezone(timezone);
    _set_daylight(daylight);
    _set_dstbias(dstbias);
}

static void __cdecl tzset_env_copy_to_tzname(const wchar_t * const tz_env, wchar_t * const wide_tzname, char * const narrow_tzname, rsize_t const tzname_length)
{
    _ERRCHECK(wcsncpy_s(wide_tzname, _TZ_STRINGS_SIZE, tz_env, tzname_length));

    // Historically when getting _tzname via TZ, the narrow environment was used to populate _tzname when getting _tzname.
    // The narrow environment is always encoded in the ACP (so _tzname was encoded in the ACP when coming from TZ), but
    // when getting _tzname from the OS, the current active code page (set via setlocale()) was used instead.
    // To maintain behavior compatibility, we remain intentionally inconsistent with
    // how _tzname is generated when getting time zone information from the OS by explicitly encoding with the ACP.
    // UTF-8 mode is opt-in, so we can correct this inconsistency when the current code page is UTF-8.

    // Invalid characters are replaced by closest approximation or default character.
    // On other failure, simply leave _tzname blank.
    __acrt_WideCharToMultiByte(
        __acrt_get_utf8_acp_compatibility_codepage(),
        0,
        wide_tzname,
        static_cast<int>(tzname_length),
        narrow_tzname,
        _TZ_STRINGS_SIZE - 1, // Leave room for null terminator
        nullptr,
        nullptr);
}

static void __cdecl tzset_from_environment_nolock(_In_z_ wchar_t* tz_env) throw()
{
    _BEGIN_SECURE_CRT_DEPRECATION_DISABLE
    char** tzname = _tzname;
    wchar_t** wide_tzname = __wide_tzname();
    _END_SECURE_CRT_DEPRECATION_DISABLE

    long timezone = 0;
    int  daylight = 0;
    _ERRCHECK(_get_timezone(&timezone));
    _ERRCHECK(_get_daylight(&daylight));

    // Check to see if the TZ value is unchanged from an earlier call to this
    // function.  If it hasn't changed, we have no work to do:
    if (last_wide_tz != nullptr && wcscmp(tz_env, last_wide_tz) == 0)
    {
        return;
    }

    // Update the global last_wide_tz variable:
    auto new_wide_tz = _malloc_crt_t(wchar_t, wcslen(tz_env) + 1);
    if (!new_wide_tz)
    {
        return;
    }

    _free_crt(last_wide_tz);
    last_wide_tz = new_wide_tz.detach();

    _ERRCHECK(wcscpy_s(last_wide_tz, wcslen(tz_env) + 1, tz_env));

    // Process TZ value and update _tzname, _timezone and _daylight.
    memset(wide_tzname[0], 0, _TZ_STRINGS_SIZE * sizeof(wchar_t));
    memset(wide_tzname[1], 0, _TZ_STRINGS_SIZE * sizeof(wchar_t));
    memset(tzname[0], 0, _TZ_STRINGS_SIZE);
    memset(tzname[1], 0, _TZ_STRINGS_SIZE);

    rsize_t const tzname_length = 3;

    // Copy standard time zone name (index 0)
    tzset_env_copy_to_tzname(tz_env, wide_tzname[0], tzname[0], tzname_length);

    // Skip first few characters if present.
    for (rsize_t i = 0; i < tzname_length; ++i)
    {
        if (*tz_env)
        {
            ++tz_env;
        }
    }

    // The time difference is of the form:
    //     [+|-]hh[:mm[:ss]]
    // Check for the minus sign first:
    bool const is_negative_difference = *tz_env == L'-';
    if (is_negative_difference)
    {
        ++tz_env;
    }

    wchar_t * dummy;
    int const decimal_base = 10;

    // process, then skip over, the hours
    timezone = wcstol(tz_env, &dummy, decimal_base) * 3600;
    while (*tz_env == '+' || (*tz_env >= L'0' && *tz_env <= L'9'))
    {
        ++tz_env;
    }


    // Check if minutes were specified:
    if (*tz_env == L':')
    {
        // Process, then skip over, the minutes
        timezone += wcstol(++tz_env, &dummy, decimal_base) * 60;
        while (*tz_env >= L'0' && *tz_env <= L'9')
        {
            ++tz_env;
        }

        // Check if seconds were specified:
        if (*tz_env == L':')
        {
            // Process, then skip over, the seconds:
            timezone += wcstol(++tz_env, &dummy, decimal_base);
            while (*tz_env >= L'0' && *tz_env <= L'9')
            {
                ++tz_env;
            }
        }
    }

    if (is_negative_difference)
    {
        timezone = -timezone;
    }

    // Finally, check for a DST zone suffix:
    daylight = *tz_env ? 1 : 0;

    if (daylight)
    {
        // Copy daylight time zone name (index 1)
        tzset_env_copy_to_tzname(tz_env, wide_tzname[1], tzname[1], tzname_length);
    }

    _set_timezone(timezone);
    _set_daylight(daylight);
}

static void __cdecl tzset_nolock() throw()
{
    // Clear the flag indicated whether GetTimeZoneInformation was used.
    tz_api_used = 0;

    // Set year fields of dststart and dstend structures to -1 to ensure
    // they are recomputed as after this
    dststart.yr = dstend.yr = -1;

    // Get the value of the TZ environment variable:
    wchar_t local_env_buffer[local_env_buffer_size];
    wchar_t* const tz_env = get_tz_environment_variable(local_env_buffer);

    // If the buffer ended up being dynamically allocated, make sure we
    // clean it up before we return:
    __crt_unique_heap_ptr<wchar_t> tz_env_cleanup(tz_env == local_env_buffer
        ? nullptr
        : tz_env);

    // If the environment variable is not available for whatever reason, update
    // without using the environment (note that unless the Desktop CRT is loaded
    // and we have access to non-MSDK APIs, we will always tak this path).
    if (tz_env == nullptr || tz_env[0] == '\0')
        return tzset_from_system_nolock();

    return tzset_from_environment_nolock(tz_env);
}



// Sets the time zone information and calculates whether we are currently in
// Daylight Savings Time.  This reads the TZ environment variable, if that
// variable exists and can be read by the process; otherwise, the system is
// queried for the current time zone state.  The _daylight, _timezone, and
// _tzname global variables are updated accordingly.
extern "C" void __cdecl _tzset()
{
    __acrt_lock(__acrt_time_lock);
    __try
    {
        tzset_nolock();
    }
    __finally
    {
        __acrt_unlock(__acrt_time_lock);
    }
}



// This function may be called to ensure that the time zone information ha sbeen
// set at least once.  If the time zone information has not yet been set, this
// function sets it.
extern "C" void __cdecl __tzset()
{
    auto const first_time = tzset_init_state.dangerous_get_state_array() + __crt_state_management::get_current_state_index();

    if (__crt_interlocked_read(first_time) != 0)
    {
        return;
    }

    __acrt_lock(__acrt_time_lock);
    __try
    {
        if (__crt_interlocked_read(first_time) != 0)
        {
            __leave;
        }

        tzset_nolock();

        _InterlockedIncrement(first_time);
    }
    __finally
    {
        __acrt_unlock(__acrt_time_lock);
    }
}



//-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
//
// The _isindst() family of functions
//
//-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
// Converts the format of a transition date specification to a value of a
// transitiondate structure.  The dststart and dstend global variables are
// filled in with the converted date.
static void __cdecl cvtdate(
    transition_type const trantype,  // start or end of DST
    date_type       const datetype,  // Day-in-month or absolute date
    int             const year,      // Year, as an offset from 1900
    int             const month,     // Month, where 0 is January
    int             const week,      // Week of month, if datetype is day-in-month
    int             const dayofweek, // Day of week, if datetype is day-in-month
    int             const date,      // Date of month (1 - 31)
    int             const hour,      // Hours (0 - 23)
    int             const min,       // Minutes (0 - 59)
    int             const sec,       // Seconds (0 - 59)
    int             const msec       // Milliseconds (0 - 999)
    ) throw()
{
    int yearday;
    int monthdow;
    long dstbias = 0;

    if (datetype == date_type::day_in_month)
    {
        // Figure out the year-day of the start of the month:
        yearday = 1 + (__crt_time_is_leap_year(year)
            ? _lpdays[month - 1]
            : _days[month - 1]);

        // Figureo ut the day of the week of the start of the month:
        monthdow = (yearday + ((year - 70) * 365) +
                    __crt_time_elapsed_leap_years(year) + _BASE_DOW) % 7;

        // Figure out the year-day of the transition date:
        if (monthdow <= dayofweek)
            yearday += (dayofweek - monthdow) + (week - 1) * 7;
        else
            yearday += (dayofweek - monthdow) + week * 7;

        // We may have to adjust the calculation above if week == 5 (meaning the
        // last instance of the day in the month).  Check if the year falls
        // beyond after month and adjust accordingly:
        int const days_to_compare = __crt_time_is_leap_year(year)
            ? _lpdays[month]
            : _days[month];

        if (week == 5 && yearday > days_to_compare)
        {
            yearday -= 7;
        }
    }
    else
    {
        yearday = __crt_time_is_leap_year(year)
            ? _lpdays[month - 1]
            : _days[month - 1];

        yearday += date;
    }

    if (trantype == transition_type::start_of_dst)
    {
        dststart.yd = yearday;
        dststart.ms = msec + (1000 * (sec + 60 * (min + 60 * hour)));

        // Set the year field of dststart so that unnecessary calls to cvtdate()
        // may be avoided:
        dststart.yr = year;
    }
    else // end_of_dst
    {
        dstend.yd = yearday;
        dstend.ms = msec + (1000 * (sec + 60 * (min + 60 * hour)));

        // The converted date is still a DST date.  We must convert to a standard
        // (local) date while being careful the millisecond field does not
        // overflow or underflow
        _ERRCHECK(_get_dstbias(&dstbias));
        dstend.ms += (dstbias * 1000);
        if (dstend.ms < 0)
        {
            dstend.ms += milliseconds_per_day;
            dstend.yd--;
        }
        else if (dstend.ms >= milliseconds_per_day)
        {
            dstend.ms -= milliseconds_per_day;
            dstend.yd++;
        }

        // Set the year field of dstend so that unnecessary calls to cvtdate()
        // may be avoided:
        dstend.yr = year;
    }

    return;
}



// Implementation Details:  Note that there are two ways that the Daylight
// Savings Time transition data may be returned by GetTimeZoneInformation.  The
// first is a day-in-month format, which is similar to what is used in the USA.
// The transition date is given as the n'th occurrence of a specified day of the
// week in a specified month.  The second is as an absolute date.  The two cases
// are distinguished by the value of the wYear field of the SYSTEMTIME structure
// (zero denotes a day-in-month format).
static int __cdecl _isindst_nolock(tm* const tb) throw()
{
    int daylight = 0;
    _ERRCHECK(_get_daylight(&daylight));
    if (daylight == 0)
        return 0;

    // Compute (or recompute) the transition dates for Daylight Savings Time
    // if necessary.  The yr fields of dststart and dstend are compared to the
    // year of interest to determine necessity.
    if (tb->tm_year != dststart.yr || tb->tm_year != dstend.yr)
    {
        if (tz_api_used)
        {
            // Convert the start of daylight savings time to dststart:
            if (tz_info.DaylightDate.wYear == 0)
            {
                cvtdate(
                    transition_type::start_of_dst,
                    date_type::day_in_month,
                    tb->tm_year,
                    tz_info.DaylightDate.wMonth,
                    tz_info.DaylightDate.wDay,
                    tz_info.DaylightDate.wDayOfWeek,
                    0,
                    tz_info.DaylightDate.wHour,
                    tz_info.DaylightDate.wMinute,
                    tz_info.DaylightDate.wSecond,
                    tz_info.DaylightDate.wMilliseconds);
            }
            else
            {
                cvtdate(
                    transition_type::start_of_dst,
                    date_type::absolute_date,
                    tb->tm_year,
                    tz_info.DaylightDate.wMonth,
                    0,
                    0,
                    tz_info.DaylightDate.wDay,
                    tz_info.DaylightDate.wHour,
                    tz_info.DaylightDate.wMinute,
                    tz_info.DaylightDate.wSecond,
                    tz_info.DaylightDate.wMilliseconds);
            }

            // Convert start of standard time to dstend:
            if (tz_info.StandardDate.wYear == 0)
            {
                cvtdate(
                    transition_type::end_of_dst,
                    date_type::day_in_month,
                    tb->tm_year,
                    tz_info.StandardDate.wMonth,
                    tz_info.StandardDate.wDay,
                    tz_info.StandardDate.wDayOfWeek,
                    0,
                    tz_info.StandardDate.wHour,
                    tz_info.StandardDate.wMinute,
                    tz_info.StandardDate.wSecond,
                    tz_info.StandardDate.wMilliseconds);
            }
            else
            {
                cvtdate(
                    transition_type::end_of_dst,
                    date_type::absolute_date,
                    tb->tm_year,
                    tz_info.StandardDate.wMonth,
                    0,
                    0,
                    tz_info.StandardDate.wDay,
                    tz_info.StandardDate.wHour,
                    tz_info.StandardDate.wMinute,
                    tz_info.StandardDate.wSecond,
                    tz_info.StandardDate.wMilliseconds);
            }
        }
        else
        {
            // The GetTimeZoneInformation API was not used, or failed.  We use
            // the USA Daylight Savings Time rules as a fallback.
            int startmonth = 3; // March
            int startweek  = 2; // Second week
            int endmonth   = 11;// November
            int endweek    = 1; // First week

            // The rules changed in 2007:
            if (107 > tb->tm_year)
            {
                startmonth = 4; // April
                startweek  = 1; // first week
                endmonth   = 10;// October
                endweek    = 5; // last week
            }

            cvtdate(
                transition_type::start_of_dst,
                date_type::day_in_month,
                tb->tm_year,
                startmonth,
                startweek,
                0, // Sunday
                0,
                2, // 02:00 (2 AM)
                0,
                0,
                0);

            cvtdate(
                transition_type::end_of_dst,
                date_type::day_in_month,
                tb->tm_year,
                endmonth,
                endweek,
                0, // Sunday
                0,
                2, // 02:00 (2 AM)
                0,
                0,
                0);
        }
    }

    // Handle simple cases first:
    if (dststart.yd < dstend.yd)
    {
        // Northern hemisphere ordering:
        if (tb->tm_yday < dststart.yd || tb->tm_yday > dstend.yd)
            return 0;

        if (tb->tm_yday > dststart.yd && tb->tm_yday < dstend.yd)
            return 1;
    }
    else
    {
        // Southern hemisphere ordering:
        if (tb->tm_yday < dstend.yd || tb->tm_yday > dststart.yd)
            return 1;

        if (tb->tm_yday > dstend.yd && tb->tm_yday < dststart.yd)
            return 0;
    }

    long const ms = 1000 * (tb->tm_sec + 60 * tb->tm_min + 3600 * tb->tm_hour);

    if (tb->tm_yday == dststart.yd)
    {

        return ms >= dststart.ms ? 1 : 0;
    }
    else
    {
        return ms < dstend.ms ? 1 : 0;
    }
}



// Tests if the time represented by the tm structure falls in Daylight Savings
// Time or not.  The Daylight Savings Time rules are obtained from the operating
// system if GetTimeZoneInformation was used by _tzset() to obtain the time zone
// information; otherwise, the USA Daylight Savings Time rules (post-1986) are
// used.
//
// Returns 1 if the time is in Daylight Savings Time; returns 0 otherwise.
extern "C" int __cdecl _isindst(tm* const tb)
{
    int retval = 0;

    __acrt_lock(__acrt_time_lock);
    __try
    {
        retval = _isindst_nolock(tb);
    }
    __finally
    {
        __acrt_unlock(__acrt_time_lock);
    }

    return retval;
}
