//
//  Copyright © 2001 - 2002, RESON Inc. All Rights Reserved.
//
//  No part of this file may be reproduced or transmitted in any form or by
//  any means, electronic or mechanical, including photocopy, recording, or
//  information storage or retrieval system, without permission in writing
//  from RESON Inc.
//
//  Filename:   SystemTime.cpp
//
//  Project:    6046.
//
//  Author(s):  W. Arcus
//
//  Purpose:    Implementation file for CSystemTime class.
//
//              This is a system helper class to allow hi-res timer to be used and support
//              synchronization of the system clock to UTC.
//
//  Notes:      
//

#include "StdAfx.h"
#include "SystemTime.h"

#pragma comment( lib, "winmm.lib" )     // Ensure we link with Microsoft's multi-media timer support.

#ifdef _DEBUG
#undef THIS_FILE
static char THIS_FILE[]=__FILE__;
#define new DEBUG_NEW
#endif

namespace                               // Begin unnamed namespace.
{

    const unsigned __int64 ui64MilliSecTo100NanoSec_c = ( unsigned __int64 ) ( 10000UL   );     // Conversion from ms to units of 100ns.
    const unsigned __int64 ui64OutOfSyncTolerance_c   = ( unsigned __int64 ) ( 5000000UL );     // +/- 0.5s in units of 100ns.

}                                       // End unnamed namespace.

//////////////////////////////////////////////////////////////////////
// CSystemTime class implementation file.

// Class scope objects.

CCritical       CSystemTime::m_StaticGuard;
int             CSystemTime::m_iRefCount                    = 0;
bool            CSystemTime::m_bHiResTimerInitialized       = false;

TIMECAPS        CSystemTime::m_sCapabilities                = { 0 };

__int64         CSystemTime::m_i64ClockFrequency            = 0;
__int64         CSystemTime::m_i64InitialCount              = 0;
DWORD           CSystemTime::m_dwOffset                     = 0UL;

CSystemTime::CSystemTime( void )
{
    ManageDllState_m();

    InitializeTimer();
}

CSystemTime::CSystemTime(   const WORD &rwYear,
                            const WORD &rwMonth,
                            const WORD &rwDay,
                            const WORD &rwHour,
                            const WORD &rwMinute,
                            const WORD &rwSecond,
                            const WORD &rwMilliseconds )
{
    ManageDllState_m();

    // Initialize the timer resolution and set UTC time environment vairables.

    InitializeTimer();

    SYSTEMTIME sSystemTime;

    // Fill out struture with current settings.

    ::GetLocalTime( &sSystemTime );

    // Update with those specified.

    sSystemTime.wYear         = rwYear;
    sSystemTime.wMonth        = rwMonth;
    sSystemTime.wDay          = rwDay;
    sSystemTime.wHour         = rwHour;
    sSystemTime.wMinute       = rwMinute;
    sSystemTime.wSecond       = rwSecond;
    sSystemTime.wMilliseconds = rwMilliseconds;

    // Update the local time.

    ::SetLocalTime( &sSystemTime );
}

CSystemTime::~CSystemTime( void )
{
    ManageDllState_m();

    try
    {
        // Restore the default timer resolution.

        RestoreTimer();
    }
    catch ( ... )
    {
        TRACE( _T( "CSystemTime::~CSystemTime(), unspecified exception caught\n" ) );
    }
}

CSystemTime::operator SYSTEMTIME () const
{
    ManageDllState_m();

    // Returns a copy of the current local time setting.

    SYSTEMTIME sSystemTime;

    ::GetLocalTime( &sSystemTime );

    return sSystemTime;
}

CSystemTime::operator double () const
{
    ManageDllState_m();

    // Returns the current time as a double.

    SYSTEMTIME      sSystemTime;

    ::GetLocalTime( &sSystemTime );

    struct tm       sTime;

    if ( sSystemTime.wYear > 1900 )
    {
        sTime.tm_year = sSystemTime.wYear - 1900;
    }
    else
    {
        sTime.tm_year = sSystemTime.wYear;
    }

    sTime.tm_mon  = sSystemTime.wMonth - 1;
    sTime.tm_mday = sSystemTime.wDay;
    sTime.tm_hour = sSystemTime.wHour;
    sTime.tm_min  = sSystemTime.wMinute;
    sTime.tm_sec  = sSystemTime.wSecond;

    sTime.tm_wday = 0;
    sTime.tm_yday = 0;
    sTime.tm_isdst= 0;

    return ( ( (double) ::mktime( &sTime ) ) + (0.001 * (double) (sSystemTime.wMilliseconds) ) );
}

CSystemTime::operator TIMECAPS () const
{
    // Provides read only access to the queried time capabilities structure.
    // eg. wPeriodMin = ((const TIMECAPS *) g_Timer)->wPeriodMin;

    ManageDllState_m();

    m_StaticGuard.Enter();
    ::TIMECAPS sCapabilities = m_sCapabilities;
    m_StaticGuard.Leave();

    return sCapabilities;
}

void CSystemTime::RestoreTimer( void ) const
{
    m_StaticGuard.Enter();
    const int iRefCount = --m_iRefCount;
    m_StaticGuard.Leave();

    if ( iRefCount == 0 )
    {
        m_StaticGuard.Enter();
        ::timeEndPeriod( m_sCapabilities.wPeriodMin );
        m_StaticGuard.Leave();
    }
}

void CSystemTime::InitializeTimer( void )
{
    // Use reference counting to ensure we only do this the first time.

    m_StaticGuard.Enter();
    const int iRefCount = m_iRefCount++;
    m_StaticGuard.Leave();

    if ( iRefCount == 0 )
    {
        // Put the standard windows multi-media timer into its highest resolution.

        m_StaticGuard.Enter();
        ::timeGetDevCaps( &m_sCapabilities, sizeof( m_sCapabilities ) );
        ::timeBeginPeriod( m_sCapabilities.wPeriodMin );
        m_StaticGuard.Leave();

        // Intialize the hi-res timer.

        InitializeHiResTimer();

        // Set the time environment vairable so that we're using UTC (GMT w/ no daylight savings).

        //_putenv("TZ=GMT0");
        //_tzset();
    }
}

///////////////////////////////////////////////////////////////////////////////
// Static members.

DWORD CSystemTime::GetTickCount( void )
{
    ManageDllState_m();

    DWORD           dwTickCount;
    LARGE_INTEGER   liCount;

    // Ensure the hi-res timer is initialized.

    InitializeHiResTimer();

    // Query the high performance timer and use it in preference to the standard Windows multi-media timer.
    // Note however, that we'll use the standard Window's timer if there's a problem such as not being supported.
    
    if ( ::QueryPerformanceCounter( &liCount ) )
    {
        m_StaticGuard.Enter();
        dwTickCount = ( (DWORD) (( *(__int64 *) &liCount  - m_i64InitialCount ) / m_i64ClockFrequency) ) + m_dwOffset;
        m_StaticGuard.Leave();
    }
    else
    {
        dwTickCount = ::GetTickCount();
    }

    //TRACE( _T( "GetTickCount(): %lu\n" ), dwTickCount );

    return dwTickCount;
}

void CSystemTime::UpdateSystemTime( const   SYSTEMTIME     &rsRequestedTime,
                                    const   DWORD          &rdwRecievedTimestampMilliseconds )
{
    // This member is used to update the current system time with the specified time strucutre.
    // It allows for message aging by using the passed time stamp that was typically
    // acquired by CSystemTime::GetTickCount() when the appropriate sync message was recieved.
    // Note also that the input requested SYSTEMTIME structure is assumed to be valid.

    ManageDllState_m();

    // Get the current time and message age.

    SYSTEMTIME  sCurrentSystemTime;
    ::GetLocalTime( &sCurrentSystemTime );

    // Remember: timestamp is in ms.

    DWORD dwAgeMilliseconds = CSystemTime::GetTickCount() - rdwRecievedTimestampMilliseconds;

    unsigned __int64 ui64AgeIn100NanoSecIncr = ui64MilliSecTo100NanoSec_c * (unsigned __int64) dwAgeMilliseconds;

    TRACE( "CSystemTime::UpdateSystemTime(), Message age: %lu ms, (%I64u ns x 100)\n", dwAgeMilliseconds, ui64AgeIn100NanoSecIncr );

    try
    {
        FILETIME sCurrentTimeAsFileTime,
                 sRequestedTimeAsFileTime;

        if ( ! ::SystemTimeToFileTime( &sCurrentSystemTime, &sCurrentTimeAsFileTime ) )
        {
            ThrowMessage_m( "SystemTimeToFileTime() failed for sCurrentSystemTime\n" );
        }

        if ( ! ::SystemTimeToFileTime( &rsRequestedTime, &sRequestedTimeAsFileTime ) )
        {
            ThrowMessage_m( "SystemTimeToFileTime() failed for rsRequestedTime\n" );
        }

        unsigned __int64 ui64DeltaTime;
        unsigned __int64 ui64CurrentTime   = *((unsigned __int64 *) &sCurrentTimeAsFileTime   );
        unsigned __int64 ui64RequestedTime = *((unsigned __int64 *) &sRequestedTimeAsFileTime );

        ui64RequestedTime += ui64AgeIn100NanoSecIncr;

        if ( ui64RequestedTime >= ui64CurrentTime )
        {
            ui64DeltaTime = ui64RequestedTime - ui64CurrentTime;
        }
        else
        {
            ui64DeltaTime = ui64CurrentTime   - ui64RequestedTime;
        }

#if 0
        TRACE( _T( "Current:   %02u/%02u/%02u,%02u:%02u:%8.5f\n"
                   "Requested: %02u/%02u/%02u,%02u:%02u:%8.5f\n" ), sCurrentSystemTime.wYear,
                                                                    sCurrentSystemTime.wMonth,
                                                                    sCurrentSystemTime.wDay,
                                                                    sCurrentSystemTime.wHour,
                                                                    sCurrentSystemTime.wMinute,
                                                                    static_cast<float>( sCurrentSystemTime.wSecond ) + 
                                                                        0.001f * static_cast<float>( sCurrentSystemTime.wMilliseconds ),
                                                                    rsRequestedTime.wYear,
                                                                    rsRequestedTime.wMonth,
                                                                    rsRequestedTime.wDay,
                                                                    rsRequestedTime.wHour,
                                                                    rsRequestedTime.wMinute,
                                                                    static_cast<float>( rsRequestedTime.wSecond ) + 
                                                                        0.001f * static_cast<float>( rsRequestedTime.wMilliseconds ) );
#endif

        if ( ui64DeltaTime > ui64OutOfSyncTolerance_c )
        {
            SYSTEMTIME sNewSystemTime;

            if ( ! ::FileTimeToSystemTime( (FILETIME *) &ui64RequestedTime, &sNewSystemTime ) )
            {
                ThrowMessage_m( "FileTimeToSystemTime(), failed for sNewSystemTime" );
            }

#if 0
            TRACE( _T( "New:       %02u/%02u/%02u,%02u:%02u:%8.5f\n" ), sNewSystemTime.wYear,
                                                                        sNewSystemTime.wMonth,
                                                                        sNewSystemTime.wDay,
                                                                        sNewSystemTime.wHour,
                                                                        sNewSystemTime.wMinute,
                                                                        static_cast<float>( sNewSystemTime.wSecond ) + 
                                                                            0.001f * static_cast<float>( sNewSystemTime.wMilliseconds ) );
#endif

            ::SetLocalTime( &sNewSystemTime );
        }
    }
    catch ( LPCTSTR lpszMessage )
    {
        TRACE( _T( "CSystemTime::UpdateSystemTime(), %s\n" ), lpszMessage );
    }
    catch ( ... )
    {
        TRACE( _T( "CSystemTime::UpdateSystemTime(), unspecified exception caught\n" ) );
    }
}

bool CSystemTime::TimeStampToSystemTime(    const unsigned long &rulTimeStamp,
                                            SYSTEMTIME          *psSystemTime )
{
    ManageDllState_m();

    // Get the current time stamps as soon as possible.

    DWORD dwCurrentTimeStamp = CSystemTime::GetTickCount();

    SYSTEMTIME sSystemTime;

    ::GetLocalTime( &sSystemTime );

    bool bSuccess = false;

    try
    {
        if ( psSystemTime != NULL )
        {
            // According to MSDN: "It is not recommended that you add and subtract values from 
            // the SYSTEMTIME structure to obtain relative times. Instead, you should:
            // Convert the SYSTEMTIME structure to a FILETIME structure,
            // Copy the resulting FILETIME structure to a ULARGE_INTEGER structure,
            // Use normal 64-bit arithmetic on the ULARGE_INTEGER value."
            //
            // Note also, FILETIME is a 64-bit value representing the number of 100-nanosecond intervals 
            // since January 1, 1601 (UTC). 

            FILETIME sTimeAsFileTime;

            // Copy over the snapshot of the system time.

            *psSystemTime = sSystemTime;

            // Retrieve the system time as a FILETIME struct.

            if ( ! ::SystemTimeToFileTime( psSystemTime, &sTimeAsFileTime ) )
            {
                ThrowMessage_m( "SystemTimeToFileTime() failed\n" );
            }

            // Subtract the number of 100 ns increments.
            // Note: the delta tickcount is in ms so conversion is: ( 10^-3 / 10^-9 ) / 100 = 10^4

            *((unsigned __int64 *)(&sTimeAsFileTime)) -= (ui64MilliSecTo100NanoSec_c * ((unsigned __int64)(dwCurrentTimeStamp - rulTimeStamp)));

            // Convert back to system time struct.

            if ( ! ::FileTimeToSystemTime( &sTimeAsFileTime, psSystemTime ) )
            {
                ThrowMessage_m( "FileTimeToSystemTime() failed\n" );
            }

            // We're done.

            bSuccess = true;
        }
    }
    catch ( LPCTSTR lpszMessage )
    {
        bSuccess = false;
        TRACE( _T( "CSystemTime::TimeStampToSystemTime(), %s\n" ), lpszMessage );
    }
    catch ( ... )
    {
        bSuccess = false;
        TRACE( _T( "CSystemTime::TimeStampToSystemTime(), unspecified exception caught\n" ) );
    }

    return bSuccess;
}

bool CSystemTime::TimeStampTo7kTime( const unsigned long &rulTimeStamp, TIME7K *psTime7k )
{
    ManageDllState_m();

    bool bSuccess = false;

    if ( psTime7k != NULL )
    {
        SYSTEMTIME sSystemTime;

        if ( CSystemTime::TimeStampToSystemTime( rulTimeStamp, &sSystemTime ) )
        {
            ::memset( psTime7k, 0x00, sizeof( TIME7K ) );

            int iJulianDay = ToJulianDay( sSystemTime.wDay, sSystemTime.wMonth, sSystemTime.wYear );

            psTime7k->m_unYear    = static_cast<unsigned short>( sSystemTime.wYear );               // Year                 u16 0 - 65535
            psTime7k->m_unDay     = static_cast<unsigned short>( iJulianDay );                      // Day                  u16 1 - 366
            psTime7k->m_fSeconds  = static_cast<float>( sSystemTime.wSecond ) +
                                        0.001f * static_cast<float>( sSystemTime.wMilliseconds );   // Seconds              f32 0.000000 - 59.000000
            psTime7k->m_ucHours   = static_cast<unsigned char> ( sSystemTime.wHour );               // Hours                u8  0 - 23
            psTime7k->m_ucMinutes = static_cast<unsigned char> ( sSystemTime.wMinute );             // Minutes              u8  0 - 59

            bSuccess = true;
        }
    }

    return bSuccess;
}

inline
double CSystemTime::TimeFromSystemTime( const SYSTEMTIME &rsSystemTime )
{
    ManageDllState_m();

    return ( (double) ( (DWORD) rsSystemTime.wHour   * 3600 +
                        (DWORD) rsSystemTime.wMinute * 60   +
                        (DWORD) rsSystemTime.wSecond        ) + 0.0001 * (double) rsSystemTime.wMilliseconds );
}

inline
void CSystemTime::InitializeHiResTimer( void )
{
    // Initialize the hi-res performance counter and reference it to the GetTickCount() timer's time base.

    m_StaticGuard.Enter();
    const bool bInitialized = m_bHiResTimerInitialized;
    m_StaticGuard.Leave();


    if ( ! bInitialized )
    {
        try
        {
            LARGE_INTEGER liCount;
	        LARGE_INTEGER liClockFrequency;

            m_StaticGuard.Enter();
            m_dwOffset = ::GetTickCount();
            m_StaticGuard.Leave();

            if ( ! ::QueryPerformanceCounter( &liCount ) )
            {
                throw GetLastError();
            }

            if ( ! ::QueryPerformanceFrequency( &liClockFrequency ) )
            {
                throw GetLastError();
            }

            // Compute the number of ticks per millisecond.

            m_StaticGuard.Enter();

            m_i64ClockFrequency = ( *(__int64 *) &liClockFrequency ) / 1000;
            m_i64InitialCount   =   *(__int64 *) &liCount;

			if ( m_i64ClockFrequency )
            {
                m_bHiResTimerInitialized = true;
			}

            m_StaticGuard.Leave();

        }
        catch ( const DWORD &rdwLastError )
        {
            TRACE( "CSystemTime::InitializeTimer(), Performance counter failed with error code: %lu.\n", rdwLastError );
        }
        catch ( ... )
        {
            TRACE( "CSystemTime::InitializeTimer(), Unspecified exception caught\n" );
        }
    }
}

int CSystemTime::ToJulianDay(   const int &riDay,
                                const int &riMonth,
                                const int &riYear )
{
    ManageDllState_m();

    int iJulianDay = DaysToEndOfMonth( riMonth - 1 ) + riDay;

    if ( IsLeapYear( riYear ) &&  ( riMonth >= 3 ) )
    {
        iJulianDay += 1;
    }

    return iJulianDay;
}

void CSystemTime::FromJulianDay(    int         &riDay,
                                    int         &riMonth,
                                    int         &riYear,
                                    const int   &riJulianDay )
{
	int iJulianDay = riJulianDay;

    if ( IsLeapYear( riYear ) )
    {
        if ( iJulianDay == 60 )
        {
            riMonth = 2;
            riDay   = 29;

            return;
        }

        if ( iJulianDay > 60 )
        {
            iJulianDay -= 1;
        }
    }

    for ( int iMonth = 1; iMonth <= 12; iMonth++ )
    {
        if ( iJulianDay <= DaysToEndOfMonth( iMonth ) )
        {
            riMonth = iMonth;
            riDay   = ( iJulianDay - DaysToEndOfMonth( iMonth - 1 ) );
            break;
        }
    }
}

inline
int CSystemTime::DaysToEndOfMonth( const int &riMonth )
{
    static const int aiMonth[] = { 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365 };

    if ( ( riMonth > 0 ) && ( riMonth <= 12 ) )
    {
        return aiMonth[ riMonth - 1 ];
    }

    return 0;
}

inline
bool CSystemTime::IsLeapYear( const int &riYear )
{
    return ( ( ( riYear % 4 ) == 0 ) && ( ( riYear % 400 ) != 0 ) );
}

