/** 
 * @file    kDateTime.cpp
 *
 * @internal
 * Copyright (C) 2008-2022 by LMI Technologies Inc
 * Licensed under the MIT License.
 * Redistributed files must retain the above copyright notice.
 */
#define K_PLATFORM
#include <kApi/Utils/kDateTime.h>

#include <kApi/Io/kSerializer.h>
#include <kApi/Threads/kTimer.h>
#include <kApi/Threads/kLock.h>

#include <stdio.h>
#include <time.h>

/*
* kDateTimeFormat enum
*/

kBeginEnumEx(k, kDateTimeFormat)
    kAddEnumerator(kDateTimeFormat, kDATE_TIME_FORMAT_FULL)
    kAddEnumerator(kDateTimeFormat, kDATE_TIME_FORMAT_SIMPLE)
    kAddEnumerator(kDateTimeFormat, kDATE_TIME_FORMAT_LOCAL)
    kAddEnumerator(kDateTimeFormat, kDATE_TIME_FORMAT_ISO_8601)
kEndEnumEx()

/*
* xkDateTimeManager static class
*/

kBeginStaticClassEx(k, xkDateTimeManager)
kEndStaticClassEx()

kFx(kStatus) xxkDateTimeManager_InitStatic()
{
    kStaticObj(xkDateTimeManager); 

    //lock required for plaforms without thread-safe calendar date-time API
    kCheck(kLock_ConstructEx(&sobj->lock, xkLOCK_OPTION_PRIORITY_INHERITANCE, kNULL));

    return kOK;
}

kFx(kStatus) xxkDateTimeManager_ReleaseStatic()
{
    kStaticObj(xkDateTimeManager); 

    kCheck(kDestroyRef(&sobj->lock));

    return kOK;
}

/*
* kDateTime structure
*/

kBeginValueEx(k, kDateTime)
    kAddFlags(kDateTime, kTYPE_FLAGS_PRIMITIVE)

    kAddPrivateVersion(kDateTime, "kdat6", "7.0.0.0", "kDateTime-0", Write, Read)

    kAddPrivateVMethod(kDateTime, kValue, VEquals)
    kAddPrivateVMethod(kDateTime, kValue, VHashCode)
kEndValueEx()

kFx(kBool) xkDateTime_VEquals(kType type, const void* value, const void* other)
{
    return *(kDateTime*)value == *(kDateTime*)other; 
}

kFx(kSize) xkDateTime_VHashCode(kType type, const void* value)
{
    return xk64s_VHashCode(kTypeOf(k64s), value);
}

kFx(kStatus) xkDateTime_Write(kType type, const void* values, kSize count, kSerializer serializer)
{
    return kSerializer_Write64sArray(serializer, (const k64s*) values, count); 
}

kFx(kStatus) xkDateTime_Read(kType type, void* values, kSize count, kSerializer serializer)
{
    return kSerializer_Read64sArray(serializer, (k64s*) values, count); 
}

kFx(kDateTime) kDateTime_Now()
{
    return kApiLib_DateTimeQueryHandler()();
}

kFx(kStatus) kDateTime_Format(kDateTime dateTime, kDateTimeFormat format, kChar* text, kSize capacity)
{
    k32s year = 0, month = 0, day = 0, dayOfWeek = 0, hour = 0, minute = 0, second = 0, microsecond = 0, utcOffset = 0; 

    kCheck(kDateTime_LocalParts(dateTime, &year, &month, &day, &dayOfWeek, &hour, &minute, &second, &microsecond, &utcOffset)); 

    kChar utcSign = (utcOffset >= 0) ? '+' : '-';
    k32s utcHour = kAbs_(utcOffset) / 60;
    k32s utcMinute = kAbs_(utcOffset) % 60;

    switch (format)
    {
        case kDATE_TIME_FORMAT_NULL:
        case kDATE_TIME_FORMAT_FULL:
            return kStrPrintf(text, capacity, "%04d-%02d-%02d %02d:%02d:%02d.%06d UTC%c%d:%02d", 
                              year, month, day, hour, minute, second, microsecond, utcSign, utcHour, utcMinute);
        case kDATE_TIME_FORMAT_SIMPLE:
            return kStrPrintf(text, capacity, "%04d-%02d-%02d %02d:%02d:%02d UTC%c%d:%02d", 
                              year, month, day, hour, minute, second, utcSign, utcHour, utcMinute);
        case kDATE_TIME_FORMAT_LOCAL:
            return kStrPrintf(text, capacity, "%04d-%02d-%02d %02d:%02d:%02d", 
                              year, month, day, hour, minute, second);
        case kDATE_TIME_FORMAT_ISO_8601:
            return kStrPrintf(text, capacity, "%04d-%02d-%02dT%02d:%02d:%02d.%06d%c%02d:%02d", 
                              year, month, day, hour, minute, second, microsecond, utcSign, utcHour, utcMinute);
        default:
            return kERROR_PARAMETER;
    }
}

kFx(kStatus) kDateTime_Parse(kDateTime* dateTime, kDateTimeFormat format, const kChar* text)
{
    k32s year = 0, month = 0, day = 0, hour = 0, minute = 0, second = 0, microsecond = 0, utcOffsetHours = 0, utcOffetMinutes = 0; 
    kChar utcOffsetSign = '\0';

    switch (format)
    {
        case kDATE_TIME_FORMAT_FULL:
            kCheckTrue(sscanf(text, "%04d-%02d-%02d %02d:%02d:%02d.%06d UTC%c%d:%02d", 
                       &year, &month, &day, &hour, &minute, &second, &microsecond, 
                       &utcOffsetSign, &utcOffsetHours, &utcOffetMinutes) == 10, kERROR_FORMAT); 
            break;
        case kDATE_TIME_FORMAT_SIMPLE:
            kCheckTrue(sscanf(text, "%04d-%02d-%02d %02d:%02d:%02d UTC%c%d:%02d", 
                       &year, &month, &day, &hour, &minute, &second, 
                       &utcOffsetSign, &utcOffsetHours, &utcOffetMinutes) == 9, kERROR_FORMAT); 
            break;
        case kDATE_TIME_FORMAT_LOCAL:
            kCheckTrue(sscanf(text, "%04d-%02d-%02d %02d:%02d:%02d", 
                       &year, &month, &day, &hour, &minute, &second) == 6, kERROR_FORMAT); 
            break;
        case kDATE_TIME_FORMAT_ISO_8601:
            kCheckTrue(sscanf(text, "%04d-%02d-%02dT%02d:%02d:%02d.%06d%c%02d:%02d", 
                       &year, &month, &day, &hour, &minute, &second, &microsecond, 
                       &utcOffsetSign, &utcOffsetHours, &utcOffetMinutes) == 10, kERROR_FORMAT); 
            break;
        default:
            //try all known formats, from most detailed to least detailed
            return (kSuccess(kDateTime_Parse(dateTime, kDATE_TIME_FORMAT_FULL, text)) ||
                    kSuccess(kDateTime_Parse(dateTime, kDATE_TIME_FORMAT_ISO_8601, text)) ||
                    kSuccess(kDateTime_Parse(dateTime, kDATE_TIME_FORMAT_SIMPLE, text)) ||
                    kSuccess(kDateTime_Parse(dateTime, kDATE_TIME_FORMAT_LOCAL, text))) ? kOK : kERROR_FORMAT;
    }

    k32s utcOffset = (utcOffsetSign == '\0') ? k32S_NULL : (((utcOffsetSign == '+') ? 1 : -1)*(utcOffsetHours*60 + utcOffetMinutes));

    return kDateTime_FromLocalParts(dateTime, year, month, day, hour, minute, second, microsecond, utcOffset);
}

#if defined(K_WINDOWS)

kFx(k64s) xkDateTime_DefaultNow()
{
    FILETIME utcFileTime;
    k64s utcWindowsTicks; 

    GetSystemTimeAsFileTime(&utcFileTime); 
    
    utcWindowsTicks  = (k64s)utcFileTime.dwHighDateTime << 32; 
    utcWindowsTicks |= (k64s)utcFileTime.dwLowDateTime; 

    return kDATE_TIME_EPOCH_WINDOWS + utcWindowsTicks/10; 
}

kFx(kStatus) kDateTime_LocalParts(kDateTime dateTime, k32s* year, k32s* month, k32s* day, k32s* dayOfWeek, k32s* hour, k32s* minute, k32s* second, k32s* microsecond, k32s* utcOffset)
{
    k64s utcWindowsTicks = 10*((dateTime - kDATE_TIME_EPOCH_WINDOWS) / 1000000 * 1000000);  //seconds only; handle microseconds below
    FILETIME utcFileTime; 
    SYSTEMTIME utcSystemTime; 
    SYSTEMTIME localSystemTime; 
    
    utcFileTime.dwHighDateTime = (DWORD) ((utcWindowsTicks >> 32) & 0xFFFFFFFF); 
    utcFileTime.dwLowDateTime = (DWORD) (utcWindowsTicks & 0xFFFFFFFF); 
    
    //convert from file system time to broken-down utc
    kCheckTrue(FileTimeToSystemTime(&utcFileTime, &utcSystemTime) != 0, kERROR_OS);
    
    //convert from broken down utc to broken down local
    kCheckTrue(SystemTimeToTzSpecificLocalTime(kNULL, &utcSystemTime, &localSystemTime) != 0, kERROR_OS);

    if (!kIsNull(year))        *year = localSystemTime.wYear; 
    if (!kIsNull(month))       *month = localSystemTime.wMonth; 
    if (!kIsNull(day))         *day = localSystemTime.wDay; 
    if (!kIsNull(dayOfWeek))   *dayOfWeek = localSystemTime.wDayOfWeek;
    if (!kIsNull(hour))        *hour = localSystemTime.wHour; 
    if (!kIsNull(minute))      *minute = localSystemTime.wMinute; 
    if (!kIsNull(second))      *second = localSystemTime.wSecond; 
    if (!kIsNull(microsecond)) *microsecond = dateTime % 1000000;   //microseconds

    if (!kIsNull(utcOffset))   
    {
        FILETIME localFileTime; 
        
        //convert from broken down local time to file system local time 
        kCheckTrue(SystemTimeToFileTime(&localSystemTime, &localFileTime) != 0, kERROR_OS);

        k64s localWindowsTicks = ((k64s)localFileTime.dwHighDateTime << 32) | localFileTime.dwLowDateTime;
         
        //utc offset is the difference beteen local ticks and utc ticks, converted to minutes
        *utcOffset = (k32s) ((localWindowsTicks - utcWindowsTicks) / (60LL*1000000*10));
    }

    return kOK;
}

kFx(kStatus) kDateTime_FromLocalParts(kDateTime* dateTime, k32s year, k32s month, k32s day, k32s hour, k32s minute, k32s second, k32s microsecond, k32s utcOffset)
{
    SYSTEMTIME systemTime; 
    SYSTEMTIME utcSystemTime; 
    FILETIME utcFileTime; 
    k64s utcWindowsTicks; 
    k64s result;

    systemTime.wYear = (WORD)(year); 
    systemTime.wMonth= (WORD)(month); 
    systemTime.wDay = (WORD)(day); 
    systemTime.wHour = (WORD)(hour); 
    systemTime.wMinute = (WORD)(minute); 
    systemTime.wSecond= (WORD)(second); 
    systemTime.wMilliseconds = 0;    //microseconds handled below

    if (utcOffset != k32S_NULL)
    { 
        //specific UTC offset provided by caller; convert broken down local to file system local time
        //without accounting for timezone; caller-specified utc offset will be subtracted below
        kCheckTrue(SystemTimeToFileTime(&systemTime, &utcFileTime) != 0, kERROR_OS); 
    }
    else
    {
        //convert from broken down local time to broken down UTC
        kCheckTrue(TzSpecificLocalTimeToSystemTime(kNULL, &systemTime, &utcSystemTime) != 0, kERROR_OS);

        //convert from broken-down UTC to file system UTC
        kCheckTrue(SystemTimeToFileTime(&utcSystemTime, &utcFileTime) != 0, kERROR_OS); 
    }

    utcWindowsTicks = (k64s)utcFileTime.dwHighDateTime << 32; 
    utcWindowsTicks |= (k64s)utcFileTime.dwLowDateTime; 

    result = utcWindowsTicks/10 + microsecond + kDATE_TIME_EPOCH_WINDOWS;

    //subtract caller-specified UTC offset
    if (utcOffset != k32S_NULL)
    {
        result -= utcOffset * 60LL * 1000000;
    }

    *dateTime = result;

    return kOK;
}

#elif defined (K_VX_KERNEL)

kFx(k64s) xkDateTime_DefaultNow()
{
    struct timeval tv;

    if (gettimeofday(&tv, NULL) != 0)
    {
        return 0; 
    }

    return kDATE_TIME_EPOCH_UNIX + 1000000ll*tv.tv_sec + tv.tv_usec;   
}

kFx(kStatus) kDateTime_LocalParts(kDateTime dateTime, k32s* year, k32s* month, k32s* day, k32s* dayOfWeek, k32s* hour, k32s* minute, k32s* second, k32s* microsecond, k32s* utcOffset)
{
    time_t rawTime = (dateTime - kDATE_TIME_EPOCH_UNIX) / 1000000;   //seconds
    struct tm localTimeInfo;

    //no timezone support; convert utc timestamp to broken down utc time
    kCheckTrue(!kIsNull(localtime_r(&rawTime, &localTimeInfo)), kERROR_OS);

    if (!kIsNull(year))            *year = localTimeInfo.tm_year + 1900;
    if (!kIsNull(month))           *month = localTimeInfo.tm_mon + 1;
    if (!kIsNull(day))             *day = localTimeInfo.tm_mday;
    if (!kIsNull(dayOfWeek))       *dayOfWeek = localTimeInfo.tm_wday;
    if (!kIsNull(hour))            *hour = localTimeInfo.tm_hour;
    if (!kIsNull(minute))          *minute = localTimeInfo.tm_min;
    if (!kIsNull(second))          *second = localTimeInfo.tm_sec;
    if (!kIsNull(microsecond))     *microsecond = dateTime % 1000000;
    if (!kIsNull(utcOffset))       *utcOffset = 0; 

    return kOK;
}

kFx(kStatus) kDateTime_FromLocalParts(kDateTime* dateTime, k32s year, k32s month, k32s day, k32s hour, k32s minute, k32s second, k32s microsecond, k32s utcOffset)
{
    struct tm localTimeInfo;
    k64s utc;
    k64s result; 
    
    localTimeInfo.tm_sec = second;
    localTimeInfo.tm_min = minute;
    localTimeInfo.tm_hour = hour;
    localTimeInfo.tm_mday = day;
    localTimeInfo.tm_mon = month - 1;
    localTimeInfo.tm_year = year - 1900;
    localTimeInfo.tm_isdst = 0;   //no timezone support

    //no timezone support; convert broken down UTC time to UTC timestamp
    kCheckTrue((utc = mktime(&localTimeInfo)) >= 0, kERROR_OS); 

    result = (utc  * 1000000) + microsecond + kDATE_TIME_EPOCH_UNIX;

    //subtract caller-specified UTC offset
    if (utcOffset != k32S_NULL)
    {
        result -= utcOffset * 60LL * 1000000;
    }

    *dateTime = result;
    
    return kOK;
}

#elif defined(K_TI_BIOS)

kFx(k64s) xkDateTime_DefaultNow()
{
    return kDATE_TIME_EPOCH_UNIX + kTimer_Now();
}

kFx(kStatus) kDateTime_LocalParts(kDateTime dateTime, k32s* year, k32s* month, k32s* day, k32s* dayOfWeek, k32s* hour, k32s* minute, k32s* second, k32s* microsecond, k32s* utcOffset)
{
    kStaticObj(xkDateTimeManager); 

    //lock required: C localtime is not thread-safe
    kLock_Enter(sobj->lock); 

    kTry
    {
        time_t rawTime = (dateTime - kDATE_TIME_EPOCH_UNIX) / 1000000;   //seconds

        //no timezone support; convert utc timestamp to broken down utc time
        struct tm* localTimeInfo = localtime(&rawTime);

        kTestTrue(!kIsNull(localTimeInfo), kERROR_OS);

        if (!kIsNull(year))            *year = localTimeInfo->tm_year + 1970;   //epoch from 1970 instead of 1900
        if (!kIsNull(month))           *month = localTimeInfo->tm_mon + 1;
        if (!kIsNull(day))             *day = localTimeInfo->tm_mday;
        if (!kIsNull(dayOfWeek))       *dayOfWeek = localTimeInfo->tm_wday;
        if (!kIsNull(hour))            *hour = localTimeInfo->tm_hour;
        if (!kIsNull(minute))          *minute = localTimeInfo->tm_min;
        if (!kIsNull(second))          *second = localTimeInfo->tm_sec;
        if (!kIsNull(microsecond))     *microsecond = dateTime % 1000000;
        if (!kIsNull(utcOffset))       *utcOffset = 0; 
    }
    kFinally
    {
        kLock_Exit(sobj->lock);

        kEndFinally();
    }

    return kOK;
}

kFx(kStatus) kDateTime_FromLocalParts(kDateTime* dateTime, k32s year, k32s month, k32s day, k32s hour, k32s minute, k32s second, k32s microsecond, k32s utcOffset)
{
    kStaticObj(xkDateTimeManager); 

    //lock required: C mktime may not be thread-safe on this platform (unknown)
    kLock_Enter(sobj->lock); 

    kTry
    {
        struct tm localTimeInfo;
        k64s utc;
        k64s result; 
        
        localTimeInfo.tm_sec = second;
        localTimeInfo.tm_min = minute;
        localTimeInfo.tm_hour = hour;
        localTimeInfo.tm_mday = day;
        localTimeInfo.tm_mon = month - 1;
        localTimeInfo.tm_year = year - 1970;     //epoch from 1970 instead of 1900
        localTimeInfo.tm_isdst = 0;   //no timezone support

        //no timezone support; convert broken down UTC time to UTC timestamp
        kTestTrue((utc = mktime(&localTimeInfo)) >= 0, kERROR_OS); 

        result = (utc  * 1000000) + microsecond + kDATE_TIME_EPOCH_UNIX;

        //subtract caller-specified UTC offset
        if (utcOffset != k32S_NULL)
        {
            result -= utcOffset * 60LL * 1000000;
        }

        *dateTime = result;    
    }
    kFinally
    {
        kLock_Exit(sobj->lock);

        kEndFinally();
    }

    return kOK;
}

#elif defined(K_POSIX)

kFx(k64s) xkDateTime_DefaultNow()
{
    struct timeval tv;

    if (gettimeofday(&tv, NULL) != 0)
    {
        return 0; 
    }

    return kDATE_TIME_EPOCH_UNIX + 1000000ll*tv.tv_sec + tv.tv_usec;   
}

kFx(kStatus) kDateTime_LocalParts(kDateTime dateTime, k32s* year, k32s* month, k32s* day, k32s* dayOfWeek, k32s* hour, k32s* minute, k32s* second, k32s* microsecond, k32s* utcOffset)
{
    time_t rawTime = (dateTime - kDATE_TIME_EPOCH_UNIX) / 1000000;   //seconds
    struct tm localTimeInfo;

    //convert utc timestamp to broken down local time
    kCheckTrue(!kIsNull(localtime_r(&rawTime, &localTimeInfo)), kERROR_OS);

    if (!kIsNull(year))            *year = localTimeInfo.tm_year + 1900;
    if (!kIsNull(month))           *month = localTimeInfo.tm_mon + 1;
    if (!kIsNull(day))             *day = localTimeInfo.tm_mday;
    if (!kIsNull(dayOfWeek))       *dayOfWeek = localTimeInfo.tm_wday;
    if (!kIsNull(hour))            *hour = localTimeInfo.tm_hour;
    if (!kIsNull(minute))          *minute = localTimeInfo.tm_min;
    if (!kIsNull(second))          *second = localTimeInfo.tm_sec;
    if (!kIsNull(microsecond))     *microsecond = dateTime % 1000000;
    if (!kIsNull(utcOffset))       *utcOffset = (k32s)localTimeInfo.tm_gmtoff / 60; 

    return kOK;
}

kFx(kStatus) kDateTime_FromLocalParts(kDateTime* dateTime, k32s year, k32s month, k32s day, k32s hour, k32s minute, k32s second, k32s microsecond, k32s utcOffset)
{
    struct tm localTimeInfo;
    k64s utc;
    k64s result; 
    
    localTimeInfo.tm_sec = second;
    localTimeInfo.tm_min = minute;
    localTimeInfo.tm_hour = hour;
    localTimeInfo.tm_mday = day;
    localTimeInfo.tm_mon = month - 1;
    localTimeInfo.tm_year = year - 1900;
    localTimeInfo.tm_isdst = -1;

    //convert broken down local time to UTC timestamp
    kCheckTrue((utc = mktime(&localTimeInfo)) >= 0, kERROR_OS); 

    result = kDATE_TIME_EPOCH_UNIX + (utc  * 1000000) + microsecond;

    //subtract caller-specified UTC offset from UTC offset in current time zone
    if (utcOffset != k32S_NULL)
    {
        k32s adjustment = localTimeInfo.tm_gmtoff/60 - utcOffset;

        result += adjustment * 60LL * 1000000;
    }

    *dateTime = result;
    
    return kOK;
}

#else

kFx(k64s) xkDateTime_DefaultNow()
{
    return kDATE_TIME_EPOCH_UNIX + kTimer_Now();
}

kFx(kStatus) kDateTime_LocalParts(kDateTime dateTime, k32s* year, k32s* month, k32s* day, k32s* dayOfWeek, k32s* hour, k32s* minute, k32s* second, k32s* microsecond, k32s* utcOffset)
{ 
    return kERROR_UNIMPLEMENTED
}

kFx(kStatus) kDateTime_FromLocalParts(kDateTime* dateTime, k32s year, k32s month, k32s day, k32s hour, k32s minute, k32s second, k32s microsecond, k32s utcOffset)
{ 
    return kERROR_UNIMPLEMENTED
}

#endif
