/*************************************************************************\
* Copyright (c) 2008 UChicago Argonne LLC, as Operator of Argonne
*     National Laboratory.
* Copyright (c) 2002 The Regents of the University of California, as
*     Operator of Los Alamos National Laboratory.
* EPICS BASE is distributed subject to a Software License Agreement found
* in file LICENSE that is included with this distribution.
\*************************************************************************/

//
// $Revision-Id$
//
// Author: Jeff Hill
//
//

//
// ANSI C
//
#include <math.h>
#include <time.h>
#include <limits.h>
#include <stdio.h>

//
// WIN32
//
#define VC_EXTRALEAN
#define STRICT
#include <windows.h>

//
// EPICS
//
#define epicsExportSharedSymbols
#include "epicsTime.h"
#include "generalTimeSup.h"
#include "epicsTimer.h"
#include "errlog.h"
#include "epicsAssert.h"
#include "epicsThread.h"

#if defined ( DEBUG )
#   define debugPrintf(argsInParen) ::printf argsInParen
#else
#   define debugPrintf(argsInParen)
#endif

static int osdTimeGetCurrent ( epicsTimeStamp *pDest );

// GNU seems to require that 64 bit constants have LL on
// them. The borland compiler fails to compile constants
// with the LL suffix. MS compiler doesnt care.
#ifdef __GNUC__
#define LL_CONSTANT(VAL) VAL ## LL
#else
#define LL_CONSTANT(VAL) VAL
#endif

// for mingw
#if !defined ( MAXLONGLONG )
#define MAXLONGLONG LL_CONSTANT(0x7fffffffffffffff)
#endif

static const LONGLONG epicsEpochInFileTime = LL_CONSTANT(0x01b41e2a18d64000);

class currentTime : public epicsTimerNotify {
public:
    currentTime ();
    ~currentTime ();
    void getCurrentTime ( epicsTimeStamp & dest );
    void startPLL ();
private:
    CRITICAL_SECTION mutex;
    LONGLONG lastPerfCounter;
    LONGLONG perfCounterFreq;
    LONGLONG epicsTimeLast; // nano-sec since the EPICS epoch
    LONGLONG perfCounterFreqPLL;
    LONGLONG lastPerfCounterPLL;
    LONGLONG lastFileTimePLL;
    epicsTimerQueueActive * pTimerQueue;
    epicsTimer * pTimer;
    bool perfCtrPresent;

    epicsTimerNotify::expireStatus expire ( const epicsTime & );
};

static currentTime * pCurrentTime = 0;
static const LONGLONG FILE_TIME_TICKS_PER_SEC = 10000000;
static const LONGLONG EPICS_TIME_TICKS_PER_SEC = 1000000000;
static const LONGLONG ET_TICKS_PER_FT_TICK =
            EPICS_TIME_TICKS_PER_SEC / FILE_TIME_TICKS_PER_SEC;

//
// Start and register time provider
//
static int timeRegister(void)
{
    pCurrentTime = new currentTime ();

    generalTimeCurrentTpRegister("PerfCounter", 150, osdTimeGetCurrent);

    pCurrentTime->startPLL ();
    return 1;
}
static int done = timeRegister();

//
// osdTimeGetCurrent ()
//
static int osdTimeGetCurrent ( epicsTimeStamp *pDest )
{
    assert ( pCurrentTime );

    pCurrentTime->getCurrentTime ( *pDest );
    return epicsTimeOK;
}

inline void UnixTimeToFileTime ( const time_t * pAnsiTime, LPFILETIME pft )
{
     LONGLONG ll = Int32x32To64 ( *pAnsiTime, 10000000 ) + LL_CONSTANT(116444736000000000);
     pft->dwLowDateTime = static_cast < DWORD > ( ll );
     pft->dwHighDateTime = static_cast < DWORD > ( ll >>32 );
}

static int daysInMonth[] = { 31, 28, 31, 30, 31, 30, 31,
    31, 30, 31, 30, 31 };

static bool isLeapYear ( DWORD year )
{
    if ( (year % 4) == 0 ) {
        return ( ( year % 100 ) != 0 || ( year % 400 ) == 0 );
    } else {
        return false;
    }
}

static int dayOfYear ( DWORD day, DWORD month, DWORD year )
{
    DWORD nDays = 0;
    for ( unsigned m = 1; m < month; m++ ) {
        nDays += daysInMonth[m-1];
        if ( m == 2 && isLeapYear(year) ) {
            nDays++;
        }
    }
    return nDays + day;
}

// synthesize a reentrant gmtime on WIN32
int epicsShareAPI epicsTime_gmtime ( const time_t *pAnsiTime, struct tm *pTM )
{
    FILETIME ft;
    UnixTimeToFileTime ( pAnsiTime, &ft );

    SYSTEMTIME st;
    BOOL status = FileTimeToSystemTime ( &ft, &st );
    if ( ! status ) {
        return epicsTimeERROR;
    }

    pTM->tm_sec = st.wSecond; // seconds after the minute - [0,59]
    pTM->tm_min = st.wMinute; // minutes after the hour - [0,59]
    pTM->tm_hour = st.wHour; // hours since midnight - [0,23]
    assert ( st.wDay >= 1 && st.wDay <= 31 );
    pTM->tm_mday = st.wDay; // day of the month - [1,31]
    assert ( st.wMonth >= 1 && st.wMonth <= 12 );
    pTM->tm_mon = st.wMonth - 1; // months since January - [0,11]
    assert ( st.wYear >= 1900 );
    pTM->tm_year = st.wYear - 1900; // years since 1900
    pTM->tm_wday = st.wDayOfWeek; // days since Sunday - [0,6]
    pTM->tm_yday = dayOfYear ( st.wDay, st.wMonth, st.wYear ) - 1;
    pTM->tm_isdst = 0;

    return epicsTimeOK;
}

// synthesize a reentrant localtime on WIN32
int epicsShareAPI epicsTime_localtime (
    const time_t * pAnsiTime, struct tm * pTM )
{
    FILETIME ft;
    UnixTimeToFileTime ( pAnsiTime, & ft );

    TIME_ZONE_INFORMATION tzInfo;
    DWORD tzStatus = GetTimeZoneInformation ( & tzInfo );
    if ( tzStatus == TIME_ZONE_ID_INVALID ) {
        return epicsTimeERROR;
    }

    //
    // There are remarkable weaknessess in the FileTimeToLocalFileTime
    // interface so we dont use it here. Unfortunately, there is no
    // corresponding function that works on file time.
    //
    SYSTEMTIME st;
    BOOL success = FileTimeToSystemTime ( & ft, & st );
    if ( ! success ) {
        return epicsTimeERROR;
    }
    SYSTEMTIME lst;
    success = SystemTimeToTzSpecificLocalTime (
        & tzInfo, & st, & lst );
    if ( ! success ) {
        return epicsTimeERROR;
    }

    //
    // We must convert back to file time so that we can determine if DST
    // is active...
    //
    FILETIME lft;
    success = SystemTimeToFileTime ( & lst, & lft );
    if ( ! success ) {
        return epicsTimeERROR;
    }

    int is_dst = -1; // unknown state of dst
    if ( tzStatus != TIME_ZONE_ID_UNKNOWN &&
            tzInfo.StandardDate.wMonth != 0 &&
            tzInfo.DaylightDate.wMonth != 0) {
        // determine if the specified date is
        // in daylight savings time
        tzInfo.StandardDate.wYear = st.wYear;
        FILETIME StandardDateFT;
        success = SystemTimeToFileTime (
            & tzInfo.StandardDate, & StandardDateFT );
        if ( ! success ) {
            return epicsTimeERROR;
        }
        tzInfo.DaylightDate.wYear = st.wYear;
        FILETIME DaylightDateFT;
        success = SystemTimeToFileTime (
            & tzInfo.DaylightDate, & DaylightDateFT );
        if ( ! success ) {
            return epicsTimeERROR;
        }
        if ( CompareFileTime ( & lft, & DaylightDateFT ) >= 0
                && CompareFileTime ( & lft, & StandardDateFT ) < 0 ) {
            is_dst = 1;
        }
        else {
            is_dst = 0;
        }
    }

    pTM->tm_sec = lst.wSecond; // seconds after the minute - [0,59]
    pTM->tm_min = lst.wMinute; // minutes after the hour - [0,59]
    pTM->tm_hour = lst.wHour; // hours since midnight - [0,23]
    assert ( lst.wDay >= 1 && lst.wDay <= 31 );
    pTM->tm_mday = lst.wDay; // day of the month - [1,31]
    assert ( lst.wMonth >= 1 && lst.wMonth <= 12 );
    pTM->tm_mon = lst.wMonth - 1; // months since January - [0,11]
    assert ( lst.wYear >= 1900 );
    pTM->tm_year = lst.wYear - 1900; // years since 1900
    pTM->tm_wday = lst.wDayOfWeek; // days since Sunday - [0,6]
    pTM->tm_yday = dayOfYear ( lst.wDay, lst.wMonth, lst.wYear ) - 1;
    pTM->tm_isdst = is_dst;

    return epicsTimeOK;
}

currentTime::currentTime () :
    lastPerfCounter ( 0 ),
    perfCounterFreq ( 0 ),
    epicsTimeLast ( 0 ),
    perfCounterFreqPLL ( 0 ),
    lastPerfCounterPLL ( 0 ),
    lastFileTimePLL ( 0 ),
    pTimerQueue ( 0 ),
    pTimer ( 0 ),
    perfCtrPresent ( false )
{
    InitializeCriticalSection ( & this->mutex );

    // avoid interruptions by briefly becoming a time critical thread
    int originalPriority = GetThreadPriority ( GetCurrentThread () );
    SetThreadPriority ( GetCurrentThread (), THREAD_PRIORITY_TIME_CRITICAL );

    FILETIME ft;
    GetSystemTimeAsFileTime ( & ft );
    LARGE_INTEGER tmp;
    QueryPerformanceCounter ( & tmp );
    this->lastPerfCounter = tmp.QuadPart;
    // if no high resolution counters then default to low res file time
    if ( QueryPerformanceFrequency ( & tmp ) ) {
        this->perfCounterFreq = tmp.QuadPart;
        this->perfCtrPresent = true;
    }
    SetThreadPriority ( GetCurrentThread (), originalPriority );

    LARGE_INTEGER liFileTime;
    liFileTime.LowPart = ft.dwLowDateTime;
    liFileTime.HighPart = ft.dwHighDateTime;

    if ( liFileTime.QuadPart >= epicsEpochInFileTime ) {
        // the windows file time has a maximum resolution of 100 nS
        // and a nominal resolution of 10 mS - 16 mS
        this->epicsTimeLast =
            ( liFileTime.QuadPart - epicsEpochInFileTime ) *
            ET_TICKS_PER_FT_TICK;
    }
    else {
        errlogPrintf (
            "win32 osdTime.cpp detected questionable "
            "system date prior to EPICS epoch\n" );
        this->epicsTimeLast = 0;
    }

    this->perfCounterFreqPLL = this->perfCounterFreq;
    this->lastPerfCounterPLL = this->lastPerfCounter;
    this->lastFileTimePLL = liFileTime.QuadPart;
}

currentTime::~currentTime ()
{
    DeleteCriticalSection ( & this->mutex );
    if ( this->pTimer ) {
        this->pTimer->destroy ();
    }
    if ( this->pTimerQueue ) {
        this->pTimerQueue->release ();
    }
}

void currentTime::getCurrentTime ( epicsTimeStamp & dest )
{
    if ( this->perfCtrPresent ) {
        EnterCriticalSection ( & this->mutex );

        LARGE_INTEGER curPerfCounter;
        QueryPerformanceCounter ( & curPerfCounter );

        LONGLONG offset;
        if ( curPerfCounter.QuadPart >= this->lastPerfCounter ) {
            offset = curPerfCounter.QuadPart - this->lastPerfCounter;
        }
        else {
            //
            // must have been a timer roll-over event
            //
            // It takes 9.223372036855e+18/perf_freq sec to roll over this
            // counter. This is currently about 245118 years using the perf
            // counter freq value on my system (1193182). Nevertheless, I
            // have code for this situation because the performance
            // counter resolution will more than likely improve over time.
            //
            offset = ( MAXLONGLONG - this->lastPerfCounter )
                                + ( curPerfCounter.QuadPart + MAXLONGLONG );
        }
        if ( offset < MAXLONGLONG / EPICS_TIME_TICKS_PER_SEC ) {
            offset *= EPICS_TIME_TICKS_PER_SEC;
            offset /= this->perfCounterFreq;
        }
        else {
            double fpOffset = static_cast < double > ( offset );
            fpOffset *= EPICS_TIME_TICKS_PER_SEC;
            fpOffset /= static_cast < double > ( this->perfCounterFreq );
            offset = static_cast < LONGLONG > ( fpOffset );
        }
        LONGLONG epicsTimeCurrent = this->epicsTimeLast + offset;
        if ( this->epicsTimeLast > epicsTimeCurrent ) {
            double diff = static_cast < double >
                ( this->epicsTimeLast - epicsTimeCurrent );
            errlogPrintf (
                "currentTime::getCurrentTime(): %f sec "
                "time discontinuity detected\n",
                diff );
        }
        this->epicsTimeLast = epicsTimeCurrent;
        this->lastPerfCounter = curPerfCounter.QuadPart;

        LeaveCriticalSection ( & this->mutex );

        dest.secPastEpoch = static_cast < epicsUInt32 >
            ( epicsTimeCurrent / EPICS_TIME_TICKS_PER_SEC );
        dest.nsec = static_cast < epicsUInt32 >
            ( epicsTimeCurrent % EPICS_TIME_TICKS_PER_SEC );
    }
    else {
        // if high resolution performance counters are not supported then
        // fall back to low res file time
        FILETIME ft;
        GetSystemTimeAsFileTime ( & ft );
        dest = epicsTime ( ft );
    }
}

//
// Maintain corrected version of the performance counter's frequency using
// a phase locked loop. This approach is similar to NTP's.
//
epicsTimerNotify::expireStatus currentTime::expire ( const epicsTime & )
{
    // avoid interruptions by briefly becoming a time critical thread
    LARGE_INTEGER curFileTime;
    LARGE_INTEGER curPerfCounter;
    {
        int originalPriority = GetThreadPriority ( GetCurrentThread () );
        SetThreadPriority ( GetCurrentThread (), THREAD_PRIORITY_TIME_CRITICAL );
        FILETIME ft;
        GetSystemTimeAsFileTime ( & ft );
        QueryPerformanceCounter ( & curPerfCounter );
        SetThreadPriority ( GetCurrentThread (), originalPriority );

        curFileTime.LowPart = ft.dwLowDateTime;
        curFileTime.HighPart = ft.dwHighDateTime;
    }

    EnterCriticalSection ( & this->mutex );

    LONGLONG perfCounterDiff;
    if ( curPerfCounter.QuadPart >= this->lastPerfCounterPLL ) {
        perfCounterDiff = curPerfCounter.QuadPart - this->lastPerfCounterPLL;
    }
    else {
        //
        // must have been a timer roll-over event
        //
        // It takes 9.223372036855e+18/perf_freq sec to roll over this
        // counter. This is currently about 245118 years using the perf
        // counter freq value on my system (1193182). Nevertheless, I
        // have code for this situation because the performance
        // counter resolution will more than likely improve over time.
        //
        perfCounterDiff = ( MAXLONGLONG - this->lastPerfCounterPLL )
                            + ( curPerfCounter.QuadPart + MAXLONGLONG );
    }
    this->lastPerfCounterPLL = curPerfCounter.QuadPart;

    LONGLONG fileTimeDiff = curFileTime.QuadPart - this->lastFileTimePLL;
    this->lastFileTimePLL = curFileTime.QuadPart;

    // discard glitches
    if ( fileTimeDiff == 0 ) {
        LeaveCriticalSection( & this->mutex );
        debugPrintf ( ( "currentTime: file time difference in PLL was zero\n" ) );
        return expireStatus ( restart, 1.0 /* sec */ );
    }

    LONGLONG freq = ( FILE_TIME_TICKS_PER_SEC * perfCounterDiff ) / fileTimeDiff;
    LONGLONG delta = freq - this->perfCounterFreqPLL;

    // discard glitches
    LONGLONG bound = this->perfCounterFreqPLL >> 10;
    if ( delta < -bound || delta > bound ) {
        LeaveCriticalSection( & this->mutex );
        debugPrintf ( ( "freq est out of bounds l=%d e=%d h=%d\n",
            static_cast < int > ( -bound ),
            static_cast < int > ( delta ),
            static_cast < int > ( bound ) ) );
        return expireStatus ( restart, 1.0 /* sec */ );
    }

    // update feedback loop estimating the performance counter's frequency
    LONGLONG feedback = delta >> 8;
    this->perfCounterFreqPLL += feedback;

    LONGLONG perfCounterDiffSinceLastFetch;
    if ( curPerfCounter.QuadPart >= this->lastPerfCounter ) {
        perfCounterDiffSinceLastFetch =
            curPerfCounter.QuadPart - this->lastPerfCounter;
    }
    else {
        //
        // must have been a timer roll-over event
        //
        // It takes 9.223372036855e+18/perf_freq sec to roll over this
        // counter. This is currently about 245118 years using the perf
        // counter freq value on my system (1193182). Nevertheless, I
        // have code for this situation because the performance
        // counter resolution will more than likely improve over time.
        //
        perfCounterDiffSinceLastFetch =
            ( MAXLONGLONG - this->lastPerfCounter )
                            + ( curPerfCounter.QuadPart + MAXLONGLONG );
    }

    // Update the current estimated time.
    this->epicsTimeLast +=
        ( perfCounterDiffSinceLastFetch * EPICS_TIME_TICKS_PER_SEC )
            / this->perfCounterFreq;
    this->lastPerfCounter = curPerfCounter.QuadPart;

    LONGLONG epicsTimeFromCurrentFileTime =
        ( curFileTime.QuadPart - epicsEpochInFileTime ) *
        ET_TICKS_PER_FT_TICK;

    delta = epicsTimeFromCurrentFileTime - this->epicsTimeLast;
    if ( delta > EPICS_TIME_TICKS_PER_SEC || delta < -EPICS_TIME_TICKS_PER_SEC ) {
        // When there is an abrupt shift in the current computed time vs
        // the time derived from the current file time then someone has
        // probabably adjusted the real time clock and the best reaction
        // is to just assume the new time base
        this->epicsTimeLast = epicsTimeFromCurrentFileTime;
        this->perfCounterFreq = this->perfCounterFreqPLL;
        debugPrintf ( ( "currentTime: did someone adjust the date?\n" ) );
    }
    else {
        // update the effective performance counter frequency that will bring
        // our calculated time base in syncy with file time one second from now.
        this->perfCounterFreq =
            ( EPICS_TIME_TICKS_PER_SEC * this->perfCounterFreqPLL )
                / ( delta + EPICS_TIME_TICKS_PER_SEC );

        // limit effective performance counter frequency rate of change
        LONGLONG lowLimit = this->perfCounterFreqPLL - bound;
        if ( this->perfCounterFreq < lowLimit ) {
            debugPrintf ( ( "currentTime: out of bounds low freq excursion %d\n",
                static_cast <int> ( lowLimit - this->perfCounterFreq ) ) );
            this->perfCounterFreq = lowLimit;
        }
        else {
            LONGLONG highLimit = this->perfCounterFreqPLL + bound;
            if ( this->perfCounterFreq > highLimit ) {
                debugPrintf ( ( "currentTime: out of bounds high freq excursion %d\n",
                    static_cast <int> ( this->perfCounterFreq - highLimit ) ) );
                this->perfCounterFreq = highLimit;
            }
        }

#       if defined ( DEBUG )
            LARGE_INTEGER sysFreq;
            QueryPerformanceFrequency ( & sysFreq );
            double freqDiff = static_cast <int>
                ( this->perfCounterFreq - sysFreq.QuadPart );
            freqDiff /= sysFreq.QuadPart;
            freqDiff *= 100.0;
            double freqEstDiff = static_cast <int>
                ( this->perfCounterFreqPLL - sysFreq.QuadPart );
            freqEstDiff /= sysFreq.QuadPart;
            freqEstDiff *= 100.0;
            debugPrintf ( ( "currentTime: freq delta %f %% freq est delta %f %% time delta %f sec\n",
                freqDiff, freqEstDiff, static_cast < double > ( delta ) / EPICS_TIME_TICKS_PER_SEC ) );
#       endif
    }

    LeaveCriticalSection ( & this->mutex );

    return expireStatus ( restart, 1.0 /* sec */ );
}

void currentTime::startPLL ()
{
    // create frequency estimation timer when needed
    if ( this->perfCtrPresent && ! this->pTimerQueue ) {
        this->pTimerQueue = & epicsTimerQueueActive::allocate ( true );
        this->pTimer      = & this->pTimerQueue->createTimer ();
        this->pTimer->start ( *this, 1.0 );
    }
}

epicsTime::operator FILETIME () const
{
    LARGE_INTEGER ftTicks;
    ftTicks.QuadPart = ( this->secPastEpoch * FILE_TIME_TICKS_PER_SEC ) +
        ( this->nSec / ET_TICKS_PER_FT_TICK );
    ftTicks.QuadPart += epicsEpochInFileTime;
    FILETIME ts;
    ts.dwLowDateTime = ftTicks.LowPart;
    ts.dwHighDateTime = ftTicks.HighPart;
    return ts;
}

epicsTime::epicsTime ( const FILETIME & ts )
{
    LARGE_INTEGER lift;
    lift.LowPart = ts.dwLowDateTime;
    lift.HighPart = ts.dwHighDateTime;
    if ( lift.QuadPart > epicsEpochInFileTime ) {
        LONGLONG fileTimeTicksSinceEpochEPICS =
            lift.QuadPart - epicsEpochInFileTime;
        this->secPastEpoch = static_cast < epicsUInt32 >
            ( fileTimeTicksSinceEpochEPICS / FILE_TIME_TICKS_PER_SEC );
        this->nSec = static_cast < epicsUInt32 >
            ( ( fileTimeTicksSinceEpochEPICS % FILE_TIME_TICKS_PER_SEC ) *
            ET_TICKS_PER_FT_TICK );
    }
    else {
        this->secPastEpoch = 0;
        this->nSec = 0;
    }
}

epicsTime & epicsTime::operator = ( const FILETIME & rhs )
{
    *this = epicsTime ( rhs );
    return *this;
}
