/* Editor Settings: expandtabs and use 4 spaces for indentation
 * ex: set softtabstop=4 tabstop=8 expandtab shiftwidth=4: *
 * -*- mode: c, c-basic-offset: 4 -*- */

/*
 * Copyright Likewise Software
 * All rights reserved.
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or (at
 * your option) any later version.
 *
 * This program 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 General Public License
 * for more details.  You should have received a copy of the GNU General
 * Public License along with this program.  If not, see
 * <http://www.gnu.org/licenses/>.
 *
 * LIKEWISE SOFTWARE MAKES THIS SOFTWARE AVAILABLE UNDER OTHER LICENSING
 * TERMS AS WELL.  IF YOU HAVE ENTERED INTO A SEPARATE LICENSE AGREEMENT
 * WITH LIKEWISE SOFTWARE, THEN YOU MAY ELECT TO USE THE SOFTWARE UNDER THE
 * TERMS OF THAT SOFTWARE LICENSE AGREEMENT INSTEAD OF THE TERMS OF THE GNU
 * GENERAL PUBLIC LICENSE, NOTWITHSTANDING THE ABOVE NOTICE.  IF YOU
 * HAVE QUESTIONS, OR WISH TO REQUEST A COPY OF THE ALTERNATE LICENSING
 * TERMS OFFERED BY LIKEWISE SOFTWARE, PLEASE CONTACT LIKEWISE SOFTWARE AT
 * license@likewisesoftware.com
 */

#include "includes.h"
#include <lwprintf.h>
#include <lw/rtlgoto.h>

NTSTATUS
WireGetCurrentNTTime(
    PLONG64 pllCurTime
    )
{
    NTSTATUS ntStatus = STATUS_SUCCESS;
    struct timeval tv = {0};

    if (gettimeofday(&tv, NULL) < 0)
    {
        ntStatus = LwErrnoToNtStatus(errno);
        BAIL_ON_NT_STATUS(ntStatus);
    }

    *pllCurTime =
        ((tv.tv_sec + WIRE_NTTIME_EPOCH_DIFFERENCE_SECS) *
                    WIRE_FACTOR_SECS_TO_HUNDREDS_OF_NANOSECS) +
        tv.tv_usec * WIRE_FACTOR_MICROSECS_TO_HUNDREDS_OF_NANOSECS;

cleanup:

    return ntStatus;

error:

    *pllCurTime = 0LL;

    goto cleanup;
}

NTSTATUS
WireNTTimeToTimeSpec(
    LONG64 llCurTime,
    struct timespec* pTimeSpec
    )
{
    pTimeSpec->tv_sec =
            (llCurTime/WIRE_FACTOR_SECS_TO_HUNDREDS_OF_NANOSECS) -
                            WIRE_NTTIME_EPOCH_DIFFERENCE_SECS;

    pTimeSpec->tv_nsec =
            (llCurTime % WIRE_FACTOR_SECS_TO_HUNDREDS_OF_NANOSECS) * 100;

    return STATUS_SUCCESS;
}

NTSTATUS
WireNTTimeToSMBDateTime(
    LONG64    llNTTime,
    PSMB_DATE pSmbDate,
    PSMB_TIME pSmbTime
    )
{
    NTSTATUS ntStatus = 0;
    time_t   timeUnix = 0;
    struct tm stTime = {0};

    timeUnix = (llNTTime /  WIRE_FACTOR_SECS_TO_HUNDREDS_OF_NANOSECS) -
                    WIRE_NTTIME_EPOCH_DIFFERENCE_SECS;

    /* Adjust to local time zone */
    timeUnix -= (mktime(gmtime_r(&timeUnix, &stTime)) - timeUnix);

    gmtime_r(&timeUnix, &stTime);

    if ((stTime.tm_year + 1900) < 1980)
    {
        /* Minimum time for SMB format is Jan 1, 1980 00:00:00 */
        pSmbDate->usDay      = 1;
        pSmbDate->usMonth    = 1;
        pSmbDate->usYear     = 0;

        pSmbTime->TwoSeconds = 0;
        pSmbTime->Minutes    = 0;
        pSmbTime->Hours      = 0;
    }
    else
    {
        pSmbDate->usDay = stTime.tm_mday;
        pSmbDate->usMonth = stTime.tm_mon + 1;
        pSmbDate->usYear = (stTime.tm_year + 1900) - 1980;

        pSmbTime->TwoSeconds = stTime.tm_sec / 2;
        pSmbTime->Minutes = stTime.tm_min;
        pSmbTime->Hours = stTime.tm_hour;
    }

    return ntStatus;
}

NTSTATUS
WireSMBDateTimeToNTTime(
    PSMB_DATE pSmbDate,
    PSMB_TIME pSmbTime,
    PLONG64   pllNTTime
    )
{
    NTSTATUS ntStatus = STATUS_SUCCESS;
    LONG64   llNTTime = 0LL;

    if (!pSmbDate || !pSmbTime)
    {
        ntStatus = STATUS_INVALID_PARAMETER;
        BAIL_ON_NT_STATUS(ntStatus);
    }

    if ((pSmbDate->usYear  > 0) &&
        (pSmbDate->usMonth > 0) &&
        (pSmbDate->usDay   > 0))
    {
        struct tm stTime   = {0};
        time_t    timeUnix = 0;

        stTime.tm_mday = pSmbDate->usDay;
        stTime.tm_mon  = pSmbDate->usMonth - 1;
        stTime.tm_year = (pSmbDate->usYear + 1980) - 1900;

        stTime.tm_sec  = pSmbTime->TwoSeconds * 2;
        stTime.tm_min  = pSmbTime->Minutes;
        stTime.tm_hour = pSmbTime->Hours;

        timeUnix = mktime(&stTime);

        llNTTime =
                (timeUnix + WIRE_NTTIME_EPOCH_DIFFERENCE_SECS) *
                            WIRE_FACTOR_SECS_TO_HUNDREDS_OF_NANOSECS;
    }

    *pllNTTime = llNTTime;

cleanup:

    return ntStatus;

error:

    *pllNTTime = 0LL;

    goto cleanup;
}

static
NTSTATUS
WireNTTimeToSMBUTime(
    LONG64 llNTTime,
    BOOLEAN bAdjustToLocalTimeZone,
    PULONG pulSmbUTime
    )
{
    NTSTATUS ntStatus = STATUS_SUCCESS;
    struct tm stTime = {0};
    time_t tSmbUTime = 0;

    /**
     * @todo - Handle overflow
     */
    tSmbUTime = (llNTTime / WIRE_FACTOR_SECS_TO_HUNDREDS_OF_NANOSECS) -
                                            WIRE_NTTIME_EPOCH_DIFFERENCE_SECS;

    if (bAdjustToLocalTimeZone)
    {
        /* Adjust the time zone */
        tSmbUTime += (tSmbUTime - mktime(gmtime_r(&tSmbUTime, &stTime)));
    }

    *pulSmbUTime = (ULONG)tSmbUTime;

    return ntStatus;
}

NTSTATUS
WireNTTimeToSMBUTimeLocal(
    LONG64 llNTTime,
    PULONG pulSmbUTime
    )
{
    return WireNTTimeToSMBUTime(llNTTime, TRUE, pulSmbUTime);
}

NTSTATUS
WireNTTimeToSMBUTimeGMT(
    LONG64 llNTTime,
    PULONG pulSmbUTime
    )
{
    return WireNTTimeToSMBUTime(llNTTime, FALSE, pulSmbUTime);
}

static
NTSTATUS
WireSMBUTimetoNTTime(
    ULONG   ulSmbUTime,
    BOOLEAN bAdjustToLocalTimeZone,
    PLONG64 pllNTTime
    )
{
    NTSTATUS ntStatus = STATUS_SUCCESS;
    struct tm stTime = {0};
    time_t tSmbUTime = ulSmbUTime;

    if (tSmbUTime == 0)
    {
        *pllNTTime = 0;
        goto cleanup;
    }

    if (bAdjustToLocalTimeZone == TRUE)
    {
        tSmbUTime -= (tSmbUTime - mktime(gmtime_r(&tSmbUTime, &stTime)));
    }

    /**
     * @todo - Handle overflow
     */
    *pllNTTime = (tSmbUTime + WIRE_NTTIME_EPOCH_DIFFERENCE_SECS) *
                                    WIRE_FACTOR_SECS_TO_HUNDREDS_OF_NANOSECS;

 cleanup:
    return ntStatus;
}

NTSTATUS
WireSMBUTimetoNTTimeLocal(
    ULONG   ulSmbUTime,
    PLONG64 pllNTTime
    )
{
    return WireSMBUTimetoNTTime(ulSmbUTime, TRUE, pllNTTime);
}

NTSTATUS
WireSMBUTimetoNTTimeGMT(
    ULONG   ulSmbUTime,
    PLONG64 pllNTTime
    )
{
    return WireSMBUTimetoNTTime(ulSmbUTime, FALSE, pllNTTime);
}

static
ULONG64
MinutesInTimeFromYearZero(
    const struct tm* tmTime
    )
{
    ULONG64 ulMinutes = 0;
    ULONG ulLeapYears = 0;
    ULONG ulYear = tmTime->tm_year + 1900;  // tm_year uses 1900 as a base year

    // We do not have to include the current year in the computation of leap
    // years.  The current year is taken into account several lines below
    // when adding tm_yday to the number of days.
    ulLeapYears = (ulYear - 1) / 4 -
                  (ulYear - 1) / 100 +
                  (ulYear - 1) / 400 +
                  1;    // The "0" leap year

    // ulMinutes variable is used to accumulate the number of minutes in the
    // computation below.
    ulMinutes = ulYear * 365 + ulLeapYears + tmTime->tm_yday;
    ulMinutes = ulMinutes * 24 + tmTime->tm_hour;
    ulMinutes = ulMinutes * 60 + tmTime->tm_min;

    return ulMinutes;
}

// Returns minutes west from UTC
NTSTATUS
WireSMBUTimeToTimeZone(
    time_t utcTime,
    PSHORT psTimezone)
{
    NTSTATUS ntStatus = STATUS_SUCCESS;
    struct tm localTime = { 0 };
    struct tm gmTime = { 0 };

    // Ignoring errors - will not fail if cannot get times from the system
    localtime_r(&utcTime, &localTime);
    gmtime_r(&utcTime, &gmTime);

    // localtime_r takes into account DST, while gmtime_r returns UTC, where
    // DST is not relevant.  Thus, the below calculation takes DST into account
    *psTimezone = MinutesInTimeFromYearZero(&gmTime) -
                  MinutesInTimeFromYearZero(&localTime);

    return ntStatus;
}

#ifdef HAVE_TIMEGM
#define my_timegm(TmTime) timegm(TmTime)
#else
static
time_t
my_timegm(
    IN const struct tm* TmTime
    )
{
    int error = 0;
    int EE = 0;
    time_t unixTime = 0;
    struct tm tmTime = *TmTime;
    time_t now = 0;
    struct tm nowTmTime = { 0 };
    struct tm* tmResult = NULL;
    time_t antiLocalTime = 0;
    time_t antiLocalNowTime = 0;

    // This code is derived from LWNetSrvGetDCTimeFromDC()

    // We need to convert a universal time in a time structure to
    // seconds since the epoch but the only function that is available
    // assumes it's a local time and applies the timezone offset.  We
    // have to determine what the offset is for today's date which could
    // vary depending on whether daylight savings time is in effect and
    // adjust the results.  It is possible that the local time and the DC
    // time are on opposite sides of the daylight savings time change
    // boundary so the result could be off by as much as an hour and may
    // may need to be adjusted again.  Note that we're saving a copy of
    // StTime because mktime has a tendency to modify the structure.

    // First, get the current time
    now = time(NULL);
    if (-1 == now)
    {
        error = errno;
        assert(error);
        GOTO_CLEANUP_EE(EE);
    }

    // Convert the current time into struct tm format to use as a baseline
    tmResult = gmtime_r(&now, &nowTmTime);
    if (!tmResult)
    {
        error = errno;
        assert(error);
        GOTO_CLEANUP_EE(EE);
    }

    // Get "reverse-local" time by using UTC struct tm time as local time
    antiLocalTime = mktime(&tmTime);
    if (-1 == antiLocalTime)
    {
        error = errno;
        assert(error);
        GOTO_CLEANUP_EE(EE);
    }

    // Get "reverse-local" of current time
    antiLocalNowTime = mktime(&nowTmTime);
    if (-1 == antiLocalNowTime)
    {
        error = errno;
        assert(error);
        GOTO_CLEANUP_EE(EE);
    }

    // Get Unix time by adjusting the "reverse-local" time by the UTC
    // "reverse-local" delta.
    unixTime = antiLocalTime + (now - antiLocalNowTime);

    // Now check for DST anomalies by comparing the struct tm version
    // of the computed Unix time with the original struct tm version.

    tmResult = gmtime_r(&unixTime, &tmTime);
    if (!tmResult)
    {
        error = errno;
        assert(error);
        GOTO_CLEANUP_EE(EE);
    }

    if (tmTime.tm_hour == 0 && TmTime->tm_hour != 0)
    {
        unixTime -= (24 - TmTime->tm_hour) * 60 * 60;
    }
    else if (tmTime.tm_hour != 0 && TmTime->tm_hour == 0)
    {
        unixTime += (24 - tmTime.tm_hour) * 60 * 60;
    }
    else
    {
        unixTime += (TmTime->tm_hour - tmTime.tm_hour) * 60 * 60;
    }

    // Fix up minutes to handle DST that is not hour-based.
    unixTime += (TmTime->tm_min - tmTime.tm_min) * 60;

cleanup:
    if (error)
    {
        LW_RTL_LOG_ERROR("errno = %d (EE = %d)", error, EE);
        unixTime = -1;
        errno = error;
    }

    return unixTime;
}
#endif

static
NTSTATUS
LwSysTimegm(
    IN const struct tm* TmTime,
    OUT time_t* UnixTime
    )
{
    NTSTATUS ntStatus = STATUS_SUCCESS;
    struct tm tmTime = *TmTime;
    time_t unixTime = 0;

    unixTime = my_timegm(&tmTime);
    if (-1 == unixTime)
    {
        ntStatus = LwErrnoToNtStatus(errno);
        LWIO_ASSERT(!NT_SUCCESS(ntStatus));
        BAIL_ON_NT_STATUS(ntStatus);
    }

error:
    if (ntStatus)
    {
        unixTime = 0;
    }

    *UnixTime = unixTime;

    return ntStatus;
}

// Converts a string to an integer, making sure that the string has the
// number of digits specified and ends with the delimiter specified.
static
NTSTATUS
WireParseDigitsInt(
    IN PCWSTR Start,
    IN DWORD NumDigits,
    IN OPTIONAL PWCHAR Delimiter,
    OUT int* Value
    )
{
    NTSTATUS ntStatus = STATUS_SUCCESS;
    PCWSTR end = NULL;
    DWORD value = 0;

    value = (DWORD)wc16stoull(Start, &end, 10);
    if (end == NULL || end - Start != NumDigits)
    {
        ntStatus = STATUS_NOT_FOUND;
        BAIL_ON_NT_STATUS(ntStatus);
    }

    if (Delimiter && end[0] != *Delimiter)
    {
        ntStatus = STATUS_NOT_FOUND;
        BAIL_ON_NT_STATUS(ntStatus);
    }

error:
    if (ntStatus)
    {
        value = 0;
    }

    *Value = value;

    return ntStatus;
}

NTSTATUS
WirePathParseOutSnapshotTime(
    IN OUT PWSTR pwszPath,
    OUT OPTIONAL PLONG64 pSnapshotTime
    )
{
    NTSTATUS ntStatus = STATUS_SUCCESS;
    WCHAR gmtToken[] = {'@', 'G', 'M', 'T', '-', '\0'};
    WCHAR dot = '.';
    WCHAR dash = '-';
    PWSTR gmtTokenStart = NULL;
    PWSTR gmtTokenEnd = NULL;
    PWSTR pathTail = NULL;
    LONG64 snapshotTime = 0;
    struct tm stTime = {0};
    time_t timeUnix = 0;

    if (!pwszPath)
    {
        ntStatus = STATUS_NOT_FOUND;
        BAIL_ON_NT_STATUS(ntStatus);
    }

    if (!LwRtlWC16StringFindSubstring(pwszPath,
                                      gmtToken,
                                      TRUE,
                                      (PCWSTR *)&gmtTokenStart) ||
        !gmtTokenStart)
    {
        ntStatus = STATUS_NOT_FOUND;
        BAIL_ON_NT_STATUS(ntStatus);
    }

    // Check the beginning and end of the @GMT token to make sure it is a
    // discrete path element and not embedded in some other string.
    if (gmtTokenStart > pwszPath &&
        !IoRtlPathIsSeparator(*(gmtTokenStart - 1)))
    {
        ntStatus = STATUS_NOT_FOUND;
        BAIL_ON_NT_STATUS(ntStatus);
    }
    if (gmtTokenStart[24] != '\0' &&
        !IoRtlPathIsSeparator(gmtTokenStart[24]))
    {
        ntStatus = STATUS_NOT_FOUND;
        BAIL_ON_NT_STATUS(ntStatus);
    }

    // @GMT token has this format: @GMT-YYYY.MM.DD-HH.MM.SS
    // YYYY
    ntStatus = WireParseDigitsInt(&gmtTokenStart[5],
                                  4,
                                  &dot,
                                  &stTime.tm_year);
    BAIL_ON_NT_STATUS(ntStatus);

    stTime.tm_year -= 1900;

    // MM
    ntStatus = WireParseDigitsInt(&gmtTokenStart[10],
                                  2,
                                  &dot,
                                  &stTime.tm_mon);
    BAIL_ON_NT_STATUS(ntStatus);

    stTime.tm_mon -= 1;

    // DD
    ntStatus = WireParseDigitsInt(&gmtTokenStart[13],
                                  2,
                                  &dash,
                                  &stTime.tm_mday);
    BAIL_ON_NT_STATUS(ntStatus);

    // HH
    ntStatus = WireParseDigitsInt(&gmtTokenStart[16],
                                  2,
                                  &dot,
                                  &stTime.tm_hour);
    BAIL_ON_NT_STATUS(ntStatus);

    // MM
    ntStatus = WireParseDigitsInt(&gmtTokenStart[19],
                                  2,
                                  &dot,
                                  &stTime.tm_min);
    BAIL_ON_NT_STATUS(ntStatus);

    // SS
    ntStatus = WireParseDigitsInt(&gmtTokenStart[22],
                                  2,
                                  NULL,
                                  &stTime.tm_sec);
    BAIL_ON_NT_STATUS(ntStatus);


    ntStatus = LwSysTimegm(&stTime, &timeUnix);
    BAIL_ON_NT_STATUS(ntStatus);

    ntStatus = WireSMBUTimetoNTTimeGMT(timeUnix, &snapshotTime);
    BAIL_ON_NT_STATUS(ntStatus);
    if (snapshotTime <= 0)
    {
        ntStatus = STATUS_INVALID_PARAMETER;
        BAIL_ON_NT_STATUS(ntStatus);
    }

    gmtTokenEnd = gmtTokenStart + 24;
    if (IoRtlPathIsSeparator(gmtTokenEnd[0]))
    {
        // There is a trailing separator, so consume it.
        gmtTokenEnd += 1;
    }
    else if (gmtTokenStart > pwszPath &&
             IoRtlPathIsSeparator(*(gmtTokenStart - 1)))
    {
        // The @GMT token is the last thing in the path.  Remove the path
        // separator that comes before it in case the client was specifying
        // a file name (as opposed to a directory).
        gmtTokenStart -= 1;
    }

    // Remove the @GMT token string from the path

    // Allocate enough memory for the part after the token
    ntStatus = LwIoAllocateMemory(
                (LwRtlWC16StringNumChars(gmtTokenEnd) + 1) * sizeof(WCHAR),
                OUT_PPVOID(&pathTail));
    BAIL_ON_NT_STATUS(ntStatus);

    wc16scpy(pathTail, gmtTokenEnd);
    wc16scpy(gmtTokenStart, pathTail);

error:
    if (ntStatus)
    {
        if (ntStatus == STATUS_NOT_FOUND)
        {
            ntStatus = STATUS_SUCCESS;
        }

        snapshotTime = 0;
    }

    *pSnapshotTime = snapshotTime;

    LWIO_SAFE_FREE_MEMORY(pathTail);
    return ntStatus;
}

NTSTATUS
NTTimeToGmtToken(
    IN LONG64 NtTime,
    IN OUT PWCHAR pTokenBuffer
    )
{
    NTSTATUS ntStatus = STATUS_SUCCESS;
    time_t smbTimeT = 0;
    ULONG smbUTime = 0;
    struct tm stTime = {0};
    size_t numChars = 0;

    LWIO_ASSERT(pTokenBuffer);

    ntStatus = WireNTTimeToSMBUTimeGMT(
                    NtTime,
                    &smbUTime);
    BAIL_ON_NT_STATUS(ntStatus);

    smbTimeT = smbUTime;
    if (NULL == gmtime_r(&smbTimeT, &stTime))
    {
        ntStatus = STATUS_INVALID_PARAMETER;
        BAIL_ON_NT_STATUS(ntStatus);
    }
    stTime.tm_year += 1900;
    stTime.tm_mon += 1;
    LwPrintfW16String(pTokenBuffer, 25, &numChars,
                      GMT_TOKEN_FORMAT_STRING,
                      stTime.tm_year, stTime.tm_mon, stTime.tm_mday,
                      stTime.tm_hour, stTime.tm_min, stTime.tm_sec);
    if (numChars != 24)
    {
        ntStatus = STATUS_INVALID_PARAMETER;
        BAIL_ON_NT_STATUS(ntStatus);
    }

error:
    if (ntStatus)
    {
        pTokenBuffer[0] = 0;
    }

    return ntStatus;
}

/*
local variables:
mode: c
c-basic-offset: 4
indent-tabs-mode: nil
tab-width: 4
end:
*/
