/*==LICENSE==*

CyanWorlds.com Engine - MMOG client, server and tools
Copyright (C) 2011  Cyan Worlds, Inc.

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 3 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/>.

You can contact Cyan Worlds, Inc. by email legal@cyan.com
 or by snail mail at:
      Cyan Worlds, Inc.
      14617 N Newport Hwy
      Mead, WA   99021

*==LICENSE==*/
/*****************************************************************************
*
*   $/Plasma20/Sources/Plasma/NucleusLib/pnAsyncCoreExe/pnAceTimer.cpp
*   
***/

#include "Pch.h"
#pragma hdrstop


/****************************************************************************
*
*   Private
*
***/

// timer callbacks
struct AsyncTimer {
    PRIORITY_TIME(AsyncTimer)   priority;
    FAsyncTimerProc             timerProc;
    FAsyncTimerProc             destroyProc;
    void *                      param;
    LINK(AsyncTimer)            deleteLink;
};

static CCritSect            s_timerCrit;
static FAsyncTimerProc      s_timerCurr;
static HANDLE               s_timerThread;
static HANDLE               s_timerEvent;
static bool                 s_running;

static PRIQDECL(
    AsyncTimer,
    PRIORITY_TIME(AsyncTimer),
    priority
) s_timerProcs;

static LISTDECL(
    AsyncTimer,
    deleteLink
) s_timerDelete;


/****************************************************************************
*
*   Timer implementation
*
***/

//===========================================================================
static void UpdateTimer (
    AsyncTimer *    timer,
    unsigned        timeMs,
    unsigned        flags
) {
    // If the timer isn't already linked then it doesn't
    // matter whether kAsyncTimerUpdateSetPriorityHigher is
    // set; just add the timer to the queue
    if (!timer->priority.IsLinked()) {
        timer->priority.Set(timeMs);
        s_timerProcs.Enqueue(timer);
    }
    else if (((flags & kAsyncTimerUpdateSetPriorityHigher) == 0)
    || !timer->priority.IsPriorityHigher(timeMs)
    ) {
        timer->priority.Set(timeMs);
    }
}

//===========================================================================
static unsigned CallTimerProc (AsyncTimer * t, FAsyncTimerProc timerProc) {
    // Cache parameters to make timer callback outside critical section
    s_timerCurr = timerProc;

    // Leave critical section to make timer callback
    s_timerCrit.Leave();

    unsigned sleepMs = s_timerCurr(t->param);
    s_timerCurr = nil;

    s_timerCrit.Enter();

    return sleepMs;
}

//===========================================================================
// inline because it is called only once
static inline unsigned RunTimers () {
    unsigned currTimeMs = TimeGetMs();
    for (;;) {
        // Delete old timers
        while (AsyncTimer * t = s_timerDelete.Head()) {
            if (t->destroyProc)
                CallTimerProc(t, t->destroyProc);
            DEL(t);
        }

        // Get first timer to run
        AsyncTimer * t = s_timerProcs.Root();
        if (!t)
            return INFINITE;

        // If it isn't time to run this timer then exit
        unsigned sleepMs;
        if (0 < (signed) (sleepMs = (unsigned) t->priority.Get() - currTimeMs))
            return sleepMs;

        // Remove from timer queue and call timer
        s_timerProcs.Dequeue();
        sleepMs = CallTimerProc(t, t->timerProc);

        // Note if return is kAsyncTimeInfinite, we do not remove the timer
        // from the queue.  Some users depend on the fact that they can
        // call AsyncTimerUpdate and not get overridden by a return from the
        // handler at the same time.

        // Requeue timer
        currTimeMs = TimeGetMs();
        if (sleepMs != kAsyncTimeInfinite)
            UpdateTimer(t, sleepMs + currTimeMs, kAsyncTimerUpdateSetPriorityHigher);
    }
}

//===========================================================================
static unsigned THREADCALL TimerThreadProc (AsyncThread *) {
    do {
        s_timerCrit.Enter();
        const unsigned sleepMs = RunTimers();
        s_timerCrit.Leave();

        WaitForSingleObject(s_timerEvent, sleepMs);
    } while (s_running);
    return 0;
}

//===========================================================================
// inline because it is called only once
static inline void InitializeTimer () {
    if (!s_timerThread) {
        s_running = true;

        s_timerEvent = CreateEvent(
            (LPSECURITY_ATTRIBUTES) nil,
            false,                  // auto-reset event
            false,                  // initial state = off
            (LPCTSTR) nil
        );
        if (!s_timerEvent)
            ErrorFatal(__LINE__, __FILE__, "CreateEvent %u", GetLastError());

        s_timerThread = (HANDLE) AsyncThreadCreate(
            TimerThreadProc,
            nil,
            L"AsyncTimerThread"
        );
    }
}


/****************************************************************************
*
*   Module functions
*
***/

//===========================================================================
void TimerDestroy (unsigned exitThreadWaitMs) {
    s_running = false;

    if (s_timerThread) {
        SetEvent(s_timerEvent);
        WaitForSingleObject(s_timerThread, exitThreadWaitMs);
        CloseHandle(s_timerThread);
        s_timerThread = nil;
    }

    if (s_timerEvent) {
        CloseHandle(s_timerEvent);
        s_timerEvent = nil;
    }

    // Cleanup any timers that have been stopped but not deleted
    s_timerCrit.Enter();
    while (AsyncTimer * t = s_timerDelete.Head()) {
        if (t->destroyProc)
            CallTimerProc(t, t->destroyProc);
        DEL(t);
    }
    s_timerCrit.Leave();

    if (AsyncTimer * timer = s_timerProcs.Root())
        ErrorFatal(__LINE__, __FILE__, "TimerProc not destroyed: %p", timer->timerProc);
}


/****************************************************************************
*
*   Exported functions
*
***/

//===========================================================================
// 1. Timer procs do not get starved by I/O, they are called periodically.
// 2. Timer procs will never be called by multiple threads simultaneously.
void AsyncTimerCreate (
    AsyncTimer **   timer,
    FAsyncTimerProc timerProc, 
    unsigned        callbackMs,
    void *          param
) {
    ASSERT(timer);
    ASSERT(timerProc);

    // Allocate timer outside critical section
    AsyncTimer * t  = NEW(AsyncTimer);
    t->timerProc    = timerProc;
    t->destroyProc  = nil;
    t->param        = param;
    t->priority.Set(TimeGetMs() + callbackMs);

    // Set result pointer before queueing timer
    // so that the value is set before a callback
    *timer = t;

    bool setEvent;
    s_timerCrit.Enter();
    {
        InitializeTimer();

        // Does this timer need to be queued?
        if (callbackMs != kAsyncTimeInfinite)
            s_timerProcs.Enqueue(t);

        // Does the timer thread need to be awakened?
        setEvent = t == s_timerProcs.Root();
    }
    s_timerCrit.Leave();

    if (setEvent)
        SetEvent(s_timerEvent);
}

//===========================================================================
// Timer procs can be in the process of getting called in
// another thread during the unregister function -- be careful!
// -- waitComplete = will wait until the timer has been unregistered and is
//    no longer in the process of being called before returning. The flag may only
//    be set by init/destruct threads, not I/O worker threads. In addition, extreme
//    care should be used to avoid a deadlock when this flag is set; in general, it
//    is a good idea not to hold any locks or critical sections when setting the flag.
void AsyncTimerDelete (
    AsyncTimer *    timer,
    unsigned        flags
) {
    // If the timer has already been destroyed then exit
    ASSERT(timer);

    // Wait for timer before exiting function?
    FAsyncTimerProc timerProc;
    if (flags & kAsyncTimerDestroyWaitComplete)
        timerProc = timer->timerProc;
    else
        timerProc = nil;

    AsyncTimerDeleteCallback(timer, nil);

    // Wait until the timer procedure completes
    if (timerProc) {
        // ensure that I/O worker threads don't call this function with waitComplete=true
        // to prevent a possible deadlock of a timer callback waiting for itself to complete
        ThreadAssertCanBlock(__FILE__, __LINE__);

        while (s_timerCurr == timerProc)
            Sleep(1);
    }
}

//===========================================================================
void AsyncTimerDeleteCallback (
    AsyncTimer *    timer,
    FAsyncTimerProc destroyProc
) {
    // If the timer has already been destroyed then exit
    ASSERT(timer);
    ASSERT(!timer->deleteLink.IsLinked());

    // Link the timer to the deletion list
    s_timerCrit.Enter();
    {
        timer->destroyProc = destroyProc;
        s_timerDelete.Link(timer);
    }
    s_timerCrit.Leave();

    // Force the timer thread to wake up and perform the deletion
    if (destroyProc)
        SetEvent(s_timerEvent);
}

//===========================================================================
// To set the time value for a timer, use this function with flags = 0.
// To set the time to MoreRecentOf(nextTimerCallbackMs, callbackMs), use SETPRIORITYHIGHER
void AsyncTimerUpdate (
    AsyncTimer *    timer,
    unsigned        callbackMs,
    unsigned        flags
) {
    ASSERT(timer);

    bool setEvent;
    s_timerCrit.Enter();
    {
        if (callbackMs != kAsyncTimeInfinite) {
            UpdateTimer(timer, callbackMs + TimeGetMs(), flags);
            setEvent = timer == s_timerProcs.Root();
        }
        else {
            if ((flags & kAsyncTimerUpdateSetPriorityHigher) == 0)
                timer->priority.Unlink();
            setEvent = false;
        }
    }
    s_timerCrit.Leave();

    if (setEvent)
        SetEvent(s_timerEvent);
}