#ifndef SENTRY_SYNC_H_INCLUDED
#define SENTRY_SYNC_H_INCLUDED

#include "sentry_boot.h"
#include "sentry_core.h"

#include <assert.h>
#include <stdio.h>

#ifdef _MSC_VER
#    define THREAD_FUNCTION_API __stdcall
#else
#    define THREAD_FUNCTION_API
#endif

#if defined(__MINGW32__) && !defined(__MINGW64__) && !defined(__clang__)
#    define UNSIGNED_MINGW unsigned
#else
#    define UNSIGNED_MINGW
#endif

// pthreads use `void *` return types, whereas windows uses `DWORD`
#ifdef SENTRY_PLATFORM_WINDOWS
#    define SENTRY_THREAD_FN static UNSIGNED_MINGW DWORD THREAD_FUNCTION_API
#else
#    define SENTRY_THREAD_FN static void *
#endif

// define a recursive mutex for all platforms
#ifdef SENTRY_PLATFORM_WINDOWS
#    if _WIN32_WINNT >= 0x0600
#        include <synchapi.h>
#    endif
#    include <winnt.h>

#    if _WIN32_WINNT < 0x0600

#        define INIT_ONCE_STATIC_INIT                                          \
            {                                                                  \
                0                                                              \
            }

typedef union {
    PVOID Ptr;
} INIT_ONCE, *PINIT_ONCE;

typedef struct {
    HANDLE Semaphore;
    HANDLE ContinueEvent;
    LONG Waiters;
    LONG Target;
} CONDITION_VARIABLE_PREVISTA, *PCONDITION_VARIABLE_PREVISTA;

typedef BOOL(WINAPI *PINIT_ONCE_FN)(
    PINIT_ONCE InitOnce, PVOID Parameter, PVOID *Context);

inline BOOL
InitOnceExecuteOnce(
    PINIT_ONCE InitOnce, PINIT_ONCE_FN InitFn, PVOID Parameter, LPVOID *Context)
{
    for (;;) {
        switch ((ULONG_PTR)InitOnce->Ptr) {
        case 0: // not started
            if (InterlockedCompareExchangePointer(
                    &InitOnce->Ptr, (PVOID)1, (PVOID)0)
                != 0) {
                break;
            }
            if (InitFn(InitOnce, Parameter, Context)) {
                InitOnce->Ptr = (PVOID)2;
                return TRUE;
            }
            InitOnce->Ptr = 0;
            return FALSE;
        case 1: // in progress
            Sleep(1);
            break;
        case 2: // completed
            return TRUE;
        default: // unexpecterd value
            return FALSE;
        }
    }
}

inline void
InitializeConditionVariable_PREVISTA(
    PCONDITION_VARIABLE_PREVISTA ConditionVariable)
{
    ConditionVariable->Target = 0;
    ConditionVariable->Waiters = 0;
    ConditionVariable->Semaphore = CreateSemaphoreW(NULL, 0, MAXLONG, NULL);
    ConditionVariable->ContinueEvent = CreateEventW(NULL, FALSE, FALSE, NULL);
}

inline BOOL
SleepConditionVariableCS_PREVISTA(
    PCONDITION_VARIABLE_PREVISTA cv, PCRITICAL_SECTION cs, DWORD timeout)
{
    DWORD result = 0;

    LeaveCriticalSection(cs);

    InterlockedIncrement((LONG *)&cv->Waiters);
    result = WaitForSingleObject(cv->Semaphore, timeout);

    // send event only on target
    if (InterlockedDecrement((LONG *)&cv->Waiters) == cv->Target
        && result == WAIT_OBJECT_0) {
        SetEvent(cv->ContinueEvent);
    }

    EnterCriticalSection(cs);

    return result;
}

inline void
WakeConditionVariable_PREVISTA(PCONDITION_VARIABLE_PREVISTA ConditionVariable)
{
    if (!ConditionVariable) {
        return;
    }

    if (ConditionVariable->Waiters == 0) {
        return;
    }

    // set target for continue event, alert it on first occurance
    ConditionVariable->Target = ConditionVariable->Waiters - 1;

    SignalObjectAndWait(ConditionVariable->Semaphore,
        ConditionVariable->ContinueEvent, INFINITE, FALSE);
}

#    endif /* _WIN32_WINNT < 0x0600 */

struct sentry__winmutex_s {
    INIT_ONCE init_once;
    CRITICAL_SECTION critical_section;
};

static inline BOOL CALLBACK
sentry__winmutex_initonce(
    PINIT_ONCE UNUSED(InitOnce), PVOID cs, PVOID *UNUSED(lpContext))
{
    InitializeCriticalSection((LPCRITICAL_SECTION)cs);
    return TRUE;
}

static inline void
sentry__winmutex_init(struct sentry__winmutex_s *mutex)
{
    InitOnceExecuteOnce(&mutex->init_once, sentry__winmutex_initonce,
        &mutex->critical_section, NULL);
}

static inline void
sentry__winmutex_lock(struct sentry__winmutex_s *mutex)
{
    InitOnceExecuteOnce(&mutex->init_once, sentry__winmutex_initonce,
        &mutex->critical_section, NULL);
    EnterCriticalSection(&mutex->critical_section);
}

typedef HANDLE sentry_threadid_t;
typedef struct sentry__winmutex_s sentry_mutex_t;
#    define SENTRY__MUTEX_INIT                                                 \
        {                                                                      \
            INIT_ONCE_STATIC_INIT, { 0 }                                       \
        }
#    define sentry__mutex_init(Lock) sentry__winmutex_init(Lock)
#    define sentry__mutex_lock(Lock) sentry__winmutex_lock(Lock)
#    define sentry__mutex_unlock(Lock)                                         \
        LeaveCriticalSection(&(Lock)->critical_section)
#    define sentry__mutex_free(Lock)                                           \
        DeleteCriticalSection(&(Lock)->critical_section)

#    define sentry__thread_init(ThreadId) *ThreadId = INVALID_HANDLE_VALUE
#    define sentry__thread_spawn(ThreadId, Func, Data)                         \
        (*ThreadId = CreateThread(NULL, 0, Func, Data, 0, NULL),               \
            *ThreadId == INVALID_HANDLE_VALUE ? 1 : 0)
#    define sentry__thread_join(ThreadId)                                      \
        WaitForSingleObject(ThreadId, INFINITE)
#    define sentry__thread_detach(ThreadId) (void)ThreadId
#    define sentry__thread_free(ThreadId)                                      \
        do {                                                                   \
            if (*ThreadId != INVALID_HANDLE_VALUE) {                           \
                CloseHandle(*ThreadId);                                        \
            }                                                                  \
            *ThreadId = INVALID_HANDLE_VALUE;                                  \
        } while (0)

#    if _WIN32_WINNT < 0x0600
typedef CONDITION_VARIABLE_PREVISTA sentry_cond_t;
#        define sentry__cond_init(CondVar)                                     \
            InitializeConditionVariable_PREVISTA(CondVar)
#        define sentry__cond_wake WakeConditionVariable_PREVISTA
#        define sentry__cond_wait_timeout(CondVar, Lock, Timeout)              \
            SleepConditionVariableCS_PREVISTA(                                 \
                CondVar, &(Lock)->critical_section, Timeout)
#    else
typedef CONDITION_VARIABLE sentry_cond_t;
#        define sentry__cond_init(CondVar) InitializeConditionVariable(CondVar)
#        define sentry__cond_wake WakeConditionVariable
#        define sentry__cond_wait_timeout(CondVar, Lock, Timeout)              \
            SleepConditionVariableCS(                                          \
                CondVar, &(Lock)->critical_section, Timeout)
#    endif
#    define sentry__cond_wait(CondVar, Lock)                                   \
        sentry__cond_wait_timeout(CondVar, Lock, INFINITE)

#else
#    include <errno.h>
#    include <pthread.h>
#    include <sys/time.h>

/* on unix systems signal handlers can interrupt anything which means that
   we're restricted in what we can do.  In particular it's possible that
   we would end up dead locking ourselves.  While we cannot fully prevent
   races we have a logic here that while the signal handler is active we're
   disabling our mutexes so that our signal handler can access what
   otherwise would be protected by the mutex but everyone else needs to wait
   for the signal handler to finish.  This is not without risk because
   another thread might still access what the mutex protects.

   We are thus taking care that whatever such mutexes protect will not make
   us crash under concurrent modifications.  The mutexes we're likely going
   to hit are the options and scope lock. */
bool sentry__block_for_signal_handler(void);
void sentry__enter_signal_handler(void);
void sentry__leave_signal_handler(void);

typedef pthread_t sentry_threadid_t;
typedef pthread_mutex_t sentry_mutex_t;
typedef pthread_cond_t sentry_cond_t;
#    ifdef SENTRY_PLATFORM_LINUX
#        ifndef PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP
// In particular musl libc does not define a recursive initializer itself.
// However, we can just define our own. Following the chain of how musl
// initializes its mutex, the attributes are the first member:
// https://git.musl-libc.org/cgit/musl/tree/src/thread/pthread_mutex_init.c#n6
// https://git.musl-libc.org/cgit/musl/tree/src/internal/pthread_impl.h#n86
// https://git.musl-libc.org/cgit/musl/tree/include/alltypes.h.in#n86
#            define PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP                     \
                {                                                              \
                    {                                                          \
                        {                                                      \
                            PTHREAD_MUTEX_RECURSIVE                            \
                        }                                                      \
                    }                                                          \
                }
#        endif
#        define SENTRY__MUTEX_INIT PTHREAD_RECURSIVE_MUTEX_INITIALIZER_NP
#    elif defined(SENTRY_PLATFORM_AIX)
// AIX lacks PTHREAD_RECURSIVE_MUTEX_INITIALIZER, though it does have at least
// PTHREAD_MUTEX_INITIALIZER. Unfortunately, it means you must call the mutex
// init function with an initialized mutexattrs set to recursive. This isn't
// workable due to all the mutexes just sitting around not initialized but
// immediately used. The fields are basically guesswork from what happens when
// you initialize the mutex "properly" but changing the bare minimum from a
// static initialization. (Don't ask me what these fields mean, the struct is
// opaquely defined as long[n] fields.)
#        define SENTRY__MUTEX_INIT                                             \
            {                                                                  \
                {                                                              \
                    0, 0, 0, 0, /*_PTH_FLAGS_INIT64*/ 1,                       \
                        PTHREAD_MUTEX_RECURSIVE                                \
                }                                                              \
            }
#    else
#        define SENTRY__MUTEX_INIT PTHREAD_RECURSIVE_MUTEX_INITIALIZER
#    endif
#    define sentry__mutex_init(Mutex)                                          \
        do {                                                                   \
            sentry_mutex_t tmp = SENTRY__MUTEX_INIT;                           \
            *(Mutex) = tmp;                                                    \
        } while (0)
#    define sentry__mutex_lock(Mutex)                                          \
        do {                                                                   \
            if (sentry__block_for_signal_handler()) {                          \
                int rv = pthread_mutex_lock(Mutex);                            \
                (void)rv;                                                      \
                assert(rv == 0);                                               \
            }                                                                  \
        } while (0)
#    define sentry__mutex_unlock(Mutex)                                        \
        do {                                                                   \
            if (sentry__block_for_signal_handler()) {                          \
                pthread_mutex_unlock(Mutex);                                   \
            }                                                                  \
        } while (0)
#    define sentry__mutex_free(Lock) pthread_mutex_destroy(Lock)

#    define sentry__cond_init(CondVar)                                         \
        do {                                                                   \
            sentry_cond_t tmp = PTHREAD_COND_INITIALIZER;                      \
            *(CondVar) = tmp;                                                  \
        } while (0)
#    define sentry__cond_wait(Cond, Mutex)                                     \
        do {                                                                   \
            if (sentry__block_for_signal_handler()) {                          \
                pthread_cond_wait(Cond, Mutex);                                \
            }                                                                  \
        } while (0)
#    define sentry__cond_wake pthread_cond_signal
#    define sentry__thread_init(ThreadId)                                      \
        memset(ThreadId, 0, sizeof(sentry_threadid_t))
#    define sentry__thread_spawn(ThreadId, Func, Data)                         \
        (pthread_create(ThreadId, NULL, Func, Data) == 0 ? 0 : 1)
#    define sentry__thread_join(ThreadId) pthread_join(ThreadId, NULL)
#    define sentry__thread_detach(ThreadId) pthread_detach(ThreadId)
#    define sentry__thread_free sentry__thread_init
#    define sentry__threadid_equal pthread_equal
#    define sentry__current_thread pthread_self

static inline int
sentry__cond_wait_timeout(
    sentry_cond_t *cv, sentry_mutex_t *mutex, uint64_t msecs)
{
    if (!sentry__block_for_signal_handler()) {
        return 0;
    }
    struct timeval now;
    struct timespec lock_time;
    gettimeofday(&now, NULL);
    lock_time.tv_sec = now.tv_sec + msecs / 1000ULL;
    lock_time.tv_nsec = (now.tv_usec + 1000ULL * (msecs % 1000)) * 1000ULL;
    return pthread_cond_timedwait(cv, mutex, &lock_time);
}
#endif

static inline long
sentry__atomic_fetch_and_add(volatile long *val, long diff)
{
#ifdef SENTRY_PLATFORM_WINDOWS
#    if SIZEOF_LONG == 8
    return InterlockedExchangeAdd64((LONG64 *)val, diff);
#    else
    return InterlockedExchangeAdd((LONG *)val, diff);
#    endif
#else
    return __atomic_fetch_add(val, diff, __ATOMIC_SEQ_CST);
#endif
}

static inline long
sentry__atomic_store(volatile long *val, long value)
{
#ifdef SENTRY_PLATFORM_WINDOWS
#    if SIZEOF_LONG == 8
    return InterlockedExchange64((LONG64 *)val, value);
#    else
    return InterlockedExchange((LONG *)val, value);
#    endif
#else
    return __atomic_exchange_n(val, value, __ATOMIC_SEQ_CST);
#endif
}

static inline long
sentry__atomic_fetch(volatile long *val)
{
    return sentry__atomic_fetch_and_add(val, 0);
}

struct sentry_bgworker_s;
typedef struct sentry_bgworker_s sentry_bgworker_t;

typedef void (*sentry_task_exec_func_t)(void *task_data, void *state);

/**
 * Creates a new background worker thread.
 *
 * This moves ownership of `state` into the background worker, which uses the
 * given `free_state` function to free that state.
 */
sentry_bgworker_t *sentry__bgworker_new(
    void *state, void (*free_state)(void *state));

/**
 * Returns a reference to the state of the worker.
 */
void *sentry__bgworker_get_state(sentry_bgworker_t *bgw);

/**
 * Drops the reference to the background worker.
 */
void sentry__bgworker_decref(sentry_bgworker_t *bgw);

/**
 * Start a new background worker thread associated with `bgw`.
 * Returns 0 on success.
 */
int sentry__bgworker_start(sentry_bgworker_t *bgw);

/**
 * This will try to flush the background worker thread queue, with a `timeout`.
 * Returns 0 on success.
 */
int sentry__bgworker_flush(sentry_bgworker_t *bgw, uint64_t timeout);

/**
 * This will try to shut down the background worker thread, with a `timeout`.
 * Returns 0 on success.
 */
int sentry__bgworker_shutdown(sentry_bgworker_t *bgw, uint64_t timeout);

/**
 * This will set a preferable thread name for background worker.
 * Should be executed before worker start
 */
void sentry__bgworker_setname(sentry_bgworker_t *bgw, const char *thread_name);

/**
 * This will submit a new task to the background thread.
 *
 * Takes ownership of `data`, freeing it using the provided `cleanup_func`.
 * Returns 0 on success.
 */
int sentry__bgworker_submit(sentry_bgworker_t *bgw,
    sentry_task_exec_func_t exec_func, void (*cleanup_func)(void *task_data),
    void *task_data);

/**
 * This function will iterate through all the current tasks of the worker
 * thread, and will call the `callback` function for each task with a matching
 * `exec_func`. The callback can return `true` to indicate if the current task
 * should be dropped from the queue.
 * The function will return the number of dropped tasks.
 */
size_t sentry__bgworker_foreach_matching(sentry_bgworker_t *bgw,
    sentry_task_exec_func_t exec_func,
    bool (*callback)(void *task_data, void *data), void *data);

#endif
