//
// Copyright(C) 2005-2014 Simon Howard
//
// 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.
//
// DESCRIPTION:
//     OPL timer thread.
//     Once started using OPL_Timer_StartThread, the thread sleeps,
//     waking up to invoke callbacks set using OPL_Timer_SetCallback.
//

#include "SDL.h"

#include "opl_timer.h"
#include "opl_queue.h"

typedef enum
{
    THREAD_STATE_STOPPED,
    THREAD_STATE_RUNNING,
    THREAD_STATE_STOPPING,
} thread_state_t;

static SDL_Thread *timer_thread = NULL;
static thread_state_t timer_thread_state;
static uint64_t current_time;

// If non-zero, callbacks are currently paused.

static int opl_timer_paused;

// Offset in microseconds to adjust time due to the fact that playback
// was paused.

static uint64_t pause_offset = 0;

// Queue of callbacks waiting to be invoked.
// The callback queue mutex is held while the callback queue structure
// or current_time is being accessed.

static opl_callback_queue_t *callback_queue;
static SDL_mutex *callback_queue_mutex;

// The timer mutex is held while timer callback functions are being
// invoked, so that the calling code can prevent clashes.

static SDL_mutex *timer_mutex;

// Returns true if there is a callback at the head of the queue ready
// to be invoked.  Otherwise, next_time is set to the time when the
// timer thread must wake up again to check.

static int CallbackWaiting(uint64_t *next_time)
{
    // If paused, just wait in 50ms increments until unpaused.
    // Update pause_offset so after we unpause, the callback 
    // times will be right.

    if (opl_timer_paused)
    {
        *next_time = current_time + 50 * OPL_MS;
        pause_offset += 50 * OPL_MS;
        return 0;
    }

    // If there are no queued callbacks, sleep for 50ms at a time
    // until a callback is added.

    if (OPL_Queue_IsEmpty(callback_queue))
    {
        *next_time = current_time + 50 * OPL_MS;
        return 0;
    }

    // Read the time of the first callback in the queue.
    // If the time for the callback has not yet arrived,
    // we must sleep until the callback time.

    *next_time = OPL_Queue_Peek(callback_queue) + pause_offset;

    return *next_time <= current_time;
}

static uint64_t GetNextTime(void)
{
    opl_callback_t callback;
    void *callback_data;
    uint64_t next_time;
    int have_callback;

    // Keep running through callbacks until there are none ready to
    // run. When we run out of callbacks, next_time will be set.

    do
    {
        SDL_LockMutex(callback_queue_mutex);

        // Check if the callback at the head of the list is ready to
        // be invoked.  If so, pop from the head of the queue.

        have_callback = CallbackWaiting(&next_time);

        if (have_callback)
        {
            OPL_Queue_Pop(callback_queue, &callback, &callback_data);
        }

        SDL_UnlockMutex(callback_queue_mutex);

        // Now invoke the callback, if we have one.
        // The timer mutex is held while the callback is invoked.

        if (have_callback)
        {
            SDL_LockMutex(timer_mutex);
            callback(callback_data);
            SDL_UnlockMutex(timer_mutex);
        }
    } while (have_callback);

    return next_time;
}

static int ThreadFunction(void *unused)
{
    uint64_t next_time;
    uint64_t now;

    // Keep running until OPL_Timer_StopThread is called.

    while (timer_thread_state == THREAD_STATE_RUNNING)
    {
        // Get the next time that we must sleep until, and
        // wait until that time.

        next_time = GetNextTime();
        now = SDL_GetTicks() * OPL_MS;

        if (next_time > now)
        {
            SDL_Delay((next_time - now) / OPL_MS);
        }

        // Update the current time.

        SDL_LockMutex(callback_queue_mutex);
        current_time = next_time;
        SDL_UnlockMutex(callback_queue_mutex);
    }

    timer_thread_state = THREAD_STATE_STOPPED;

    return 0;
}

static void InitResources(void)
{
    callback_queue = OPL_Queue_Create();
    timer_mutex = SDL_CreateMutex();
    callback_queue_mutex = SDL_CreateMutex();
}

static void FreeResources(void)
{
    OPL_Queue_Destroy(callback_queue);
    SDL_DestroyMutex(callback_queue_mutex);
    SDL_DestroyMutex(timer_mutex);
}

int OPL_Timer_StartThread(void)
{
    InitResources();

    timer_thread_state = THREAD_STATE_RUNNING;
    current_time = SDL_GetTicks();
    opl_timer_paused = 0;
    pause_offset = 0;

    timer_thread = SDL_CreateThread(ThreadFunction, "OPL timer thread", NULL);

    if (timer_thread == NULL)
    {
        timer_thread_state = THREAD_STATE_STOPPED;
        FreeResources();

        return 0;
    }

    return 1;
}

void OPL_Timer_StopThread(void)
{
    timer_thread_state = THREAD_STATE_STOPPING;

    while (timer_thread_state != THREAD_STATE_STOPPED)
    {
        SDL_Delay(1);
    }

    FreeResources();
}

void OPL_Timer_SetCallback(uint64_t us, opl_callback_t callback, void *data)
{
    SDL_LockMutex(callback_queue_mutex);
    OPL_Queue_Push(callback_queue, callback, data,
                   current_time + us - pause_offset);
    SDL_UnlockMutex(callback_queue_mutex);
}

void OPL_Timer_ClearCallbacks(void)
{
    SDL_LockMutex(callback_queue_mutex);
    OPL_Queue_Clear(callback_queue);
    SDL_UnlockMutex(callback_queue_mutex);
}

void OPL_Timer_AdjustCallbacks(float factor)
{
    SDL_LockMutex(callback_queue_mutex);
    OPL_Queue_AdjustCallbacks(callback_queue, current_time, factor);
    SDL_UnlockMutex(callback_queue_mutex);
}

void OPL_Timer_Lock(void)
{
    SDL_LockMutex(timer_mutex);
}

void OPL_Timer_Unlock(void)
{
    SDL_UnlockMutex(timer_mutex);
}

void OPL_Timer_SetPaused(int paused)
{
    SDL_LockMutex(callback_queue_mutex);
    opl_timer_paused = paused;
    SDL_UnlockMutex(callback_queue_mutex);
}

