#include "runtime.h"
#include "threading.h"

#if defined(_WIN32)

#define WIN32_LEAN_AND_MEAN
#include <Windows.h>

struct rt_thread_s {
    HANDLE handle;

    ptrdiff_t next_reusable;

    rt_thread_entry_fn *entry;
    void *param;

    bool needs_join;
};

#define MAX_THREADS 256
static rt_thread _threads[MAX_THREADS];
static ptrdiff_t _first_reusable = MAX_THREADS;
static ptrdiff_t _next           = 0;

static HANDLE _guard;
static INIT_ONCE _guard_init = INIT_ONCE_STATIC_INIT;

static rt_thread_id _main_thread_id;

/* Called by the runtime during setup */
extern void SetMainThreadId(void) {
    _main_thread_id = (rt_thread_id)GetCurrentThreadId();
}

static BOOL CALLBACK InitGuardFn(PINIT_ONCE initOnce, PVOID parameter, PVOID *context) {
    RT_UNUSED(initOnce);
    RT_UNUSED(parameter);
    RT_UNUSED(context);

    _guard = CreateMutexW(NULL, FALSE, NULL);
    return _guard != NULL;
}

static DWORD WINAPI win32ThreadWrapper(LPVOID arg) {
    rt_thread *user_thread  = arg;
    user_thread->needs_join = false;
    user_thread->entry(user_thread->param);
    user_thread->needs_join = true;
    return 0;
}

RT_DLLEXPORT rt_thread *rtSpawnThread(rt_thread_entry_fn *entry, void *param, const char *name) {

    if (!InitOnceExecuteOnce(&_guard_init, InitGuardFn, NULL, NULL)) {
        rtReportError("core", "Failed to initialize the guard mutex.");
        return NULL;
    }

    rt_thread *thrd = NULL;
    if (WaitForSingleObject(_guard, INFINITE) != WAIT_OBJECT_0) {
        rtLog("core", "Failed to lock the guard variable: %u", GetLastError());
        return NULL;
    }
    if (_first_reusable < MAX_THREADS) {
        thrd            = &_threads[_first_reusable];
        _first_reusable = thrd->next_reusable;
        if (thrd->needs_join) {
            WaitForSingleObject(thrd->handle, INFINITE);
            CloseHandle(thrd->handle);
            thrd->needs_join = false;
        }
    } else if (_next < MAX_THREADS) {
        thrd                = &_threads[_next];
        thrd->next_reusable = MAX_THREADS;
        ++_next;
    }
    if (thrd) {
        thrd->entry  = entry;
        thrd->param  = param;
        thrd->handle = CreateThread(NULL, 0, win32ThreadWrapper, (LPVOID)thrd, 0, NULL);
        if (thrd->handle == NULL) {
            rtLog("core", "Thread creation failed");
            thrd = NULL;
        }

        WCHAR wname[64];
        if (thrd && name && rtUTF8ToWStr(name, wname, sizeof(wname)) == RT_SUCCESS)
            SetThreadDescription(thrd->handle, wname);

    } else {
        rtReportError("core", "Ran out of thread objects");
    }
    ReleaseMutex(_guard);
    return thrd;
}

RT_DLLEXPORT void rtJoinThread(rt_thread *thread) {
    WaitForSingleObject(thread->handle, INFINITE);
    CloseHandle(thread->handle);
    thread->needs_join = false;
    ptrdiff_t index    = thread - &_threads[0];

    if (WaitForSingleObject(_guard, INFINITE) != WAIT_OBJECT_0) {
        rtLog("core", "Failed to lock the guard variable: %u", GetLastError());
        return;
    }
    thread->next_reusable = _first_reusable;
    _first_reusable       = index;
    ReleaseMutex(_guard);
}

RT_DLLEXPORT unsigned int rtGetCPUCoreCount(void) {
    SYSTEM_INFO info;
    GetSystemInfo(&info);
    return (unsigned int)info.dwNumberOfProcessors;
}

RT_DLLEXPORT rt_thread_id rtGetCurrentThreadId(void) {
    return (rt_thread_id)GetCurrentThreadId();
}

RT_DLLEXPORT bool rtIsMainThread(void) {
    return rtGetCurrentThreadId() == _main_thread_id;
}

RT_DLLEXPORT void rtSleep(unsigned int milliseconds) {
    SleepEx(milliseconds, TRUE);
}

#elif defined(__linux__)

#include <pthread.h>
#include <sys/types.h>
#include <unistd.h>
#if _POSIX_C_SOURCE >= 199309L
#include <time.h>
#endif

struct rt_thread_s {
    pthread_t handle;
    ptrdiff_t next_reusable;

    rt_thread_entry_fn *entry;
    void *param;

    bool needs_join;
};

#define MAX_THREADS 256
static rt_thread _threads[MAX_THREADS];
static ptrdiff_t _first_reusable = MAX_THREADS;
static ptrdiff_t _next           = 0;

static rt_thread_id _main_thread_id;

static pthread_mutex_t _guard = PTHREAD_MUTEX_INITIALIZER;

extern void SetMainThreadId(void) {
    _main_thread_id = (rt_thread_id)gettid();
}

static void *linuxThreadWrapper(void *arg) {
    rt_thread *user_thread  = arg;
    user_thread->needs_join = false;
    user_thread->entry(user_thread->param);
    user_thread->needs_join = true;
    return NULL;
}

RT_DLLEXPORT rt_thread *rtSpawnThread(rt_thread_entry_fn *entry, void *param, const char *name) {
    rt_thread *thrd = NULL;
    pthread_mutex_lock(&_guard);
    if (_first_reusable < MAX_THREADS) {
        thrd            = &_threads[_first_reusable];
        _first_reusable = thrd->next_reusable;
        if (thrd->needs_join) {
            pthread_join(thrd->handle, NULL);
            thrd->needs_join = false;
        }
    } else if (_next < MAX_THREADS) {
        thrd                = &_threads[_next];
        thrd->next_reusable = MAX_THREADS;
        ++_next;
    }
    if (thrd) {
        thrd->entry = entry;
        thrd->param = param;
        if (pthread_create(&thrd->handle, NULL, linuxThreadWrapper, thrd) != 0) {
            rtLog("core", "Thread creation failed");
            thrd = NULL;
        }
        if (thrd && name)
            pthread_setname_np(thrd->handle, name);
    } else {
        rtReportError("core", "Ran out of thread objects");
    }
    pthread_mutex_unlock(&_guard);
    return thrd;
}

RT_DLLEXPORT void rtJoinThread(rt_thread *thread) {
    pthread_join(thread->handle, NULL);
    thread->needs_join = false;
    ptrdiff_t index    = thread - &_threads[0];
    pthread_mutex_lock(&_guard);
    thread->next_reusable = _first_reusable;
    _first_reusable       = index;
    pthread_mutex_unlock(&_guard);
}

RT_DLLEXPORT unsigned int rtGetCPUCoreCount(void) {
    int n = (int)sysconf(_SC_NPROCESSORS_ONLN);
    return (n > 0) ? (unsigned int)n : (unsigned int)sysconf(_SC_NPROCESSORS_CONF);
}

RT_DLLEXPORT rt_thread_id rtGetCurrentThreadId(void) {
    return (rt_thread_id)gettid();
}

RT_DLLEXPORT bool rtIsMainThread(void) {
    return rtGetCurrentThreadId() == _main_thread_id;
}

RT_DLLEXPORT void rtSleep(unsigned int milliseconds) {
#if _POSIX_C_SOURCE >= 199309L
    struct timespec ts;
    ts.tv_sec  = milliseconds / 1000;
    ts.tv_nsec = (milliseconds % 1000) * 1000000;
    nanosleep(&ts, NULL);
#else
    if (milliseconds >= 1000)
        sleep(milliseconds / 1000);
    usleep((milliseconds % 1000) * 1000);
#endif
}

#endif
