#include "thread.h"

#include <assert.h>
#include <stdint.h>
#include <stdlib.h>
#include <string.h>
#include <SDL2/SDL_thread.h>

#include "util/log.h"

sc_thread_id SC_MAIN_THREAD_ID;

bool
sc_thread_create(sc_thread *thread, sc_thread_fn fn, const char *name,
                 void *userdata) {
    // The thread name length is limited on some systems. Never use a name
    // longer than 16 bytes (including the final '\0')
    assert(strlen(name) <= 15);

    SDL_Thread *sdl_thread = SDL_CreateThread(fn, name, userdata);
    if (!sdl_thread) {
        LOG_OOM();
        return false;
    }

    thread->thread = sdl_thread;
    return true;
}

static SDL_ThreadPriority
to_sdl_thread_priority(enum sc_thread_priority priority) {
    switch (priority) {
        case SC_THREAD_PRIORITY_TIME_CRITICAL:
#ifdef SCRCPY_SDL_HAS_THREAD_PRIORITY_TIME_CRITICAL
            return SDL_THREAD_PRIORITY_TIME_CRITICAL;
#else
            // fall through
#endif
        case SC_THREAD_PRIORITY_HIGH:
            return SDL_THREAD_PRIORITY_HIGH;
        case SC_THREAD_PRIORITY_NORMAL:
            return SDL_THREAD_PRIORITY_NORMAL;
        case SC_THREAD_PRIORITY_LOW:
            return SDL_THREAD_PRIORITY_LOW;
        default:
            assert(!"Unknown thread priority");
            return 0;
    }
}

bool
sc_thread_set_priority(enum sc_thread_priority priority) {
    SDL_ThreadPriority sdl_priority = to_sdl_thread_priority(priority);
    int r = SDL_SetThreadPriority(sdl_priority);
    if (r) {
        LOGD("Could not set thread priority: %s", SDL_GetError());
        return false;
    }

    return true;
}

void
sc_thread_join(sc_thread *thread, int *status) {
    SDL_WaitThread(thread->thread, status);
}

bool
sc_mutex_init(sc_mutex *mutex) {
    SDL_mutex *sdl_mutex = SDL_CreateMutex();
    if (!sdl_mutex) {
        LOG_OOM();
        return false;
    }

    mutex->mutex = sdl_mutex;
#ifndef NDEBUG
    atomic_init(&mutex->locker, 0);
#endif
    return true;
}

void
sc_mutex_destroy(sc_mutex *mutex) {
    SDL_DestroyMutex(mutex->mutex);
}

void
sc_mutex_lock(sc_mutex *mutex) {
    // SDL mutexes are recursive, but we don't want to use recursive mutexes
    assert(!sc_mutex_held(mutex));
    int r = SDL_LockMutex(mutex->mutex);
#ifndef NDEBUG
    if (r) {
        LOGE("Could not lock mutex: %s", SDL_GetError());
        abort();
    }

    atomic_store_explicit(&mutex->locker, sc_thread_get_id(),
                          memory_order_relaxed);
#else
    (void) r;
#endif
}

void
sc_mutex_unlock(sc_mutex *mutex) {
#ifndef NDEBUG
    assert(sc_mutex_held(mutex));
    atomic_store_explicit(&mutex->locker, 0, memory_order_relaxed);
#endif
    int r = SDL_UnlockMutex(mutex->mutex);
#ifndef NDEBUG
    if (r) {
        LOGE("Could not lock mutex: %s", SDL_GetError());
        abort();
    }
#else
    (void) r;
#endif
}

sc_thread_id
sc_thread_get_id(void) {
    return SDL_ThreadID();
}

#ifndef NDEBUG
bool
sc_mutex_held(struct sc_mutex *mutex) {
    sc_thread_id locker_id =
        atomic_load_explicit(&mutex->locker, memory_order_relaxed);
    return locker_id == sc_thread_get_id();
}
#endif

bool
sc_cond_init(sc_cond *cond) {
    SDL_cond *sdl_cond = SDL_CreateCond();
    if (!sdl_cond) {
        LOG_OOM();
        return false;
    }

    cond->cond = sdl_cond;
    return true;
}

void
sc_cond_destroy(sc_cond *cond) {
    SDL_DestroyCond(cond->cond);
}

void
sc_cond_wait(sc_cond *cond, sc_mutex *mutex) {
    int r = SDL_CondWait(cond->cond, mutex->mutex);
#ifndef NDEBUG
    if (r) {
        LOGE("Could not wait on condition: %s", SDL_GetError());
        abort();
    }

    atomic_store_explicit(&mutex->locker, sc_thread_get_id(),
                          memory_order_relaxed);
#else
    (void) r;
#endif
}

bool
sc_cond_timedwait(sc_cond *cond, sc_mutex *mutex, sc_tick deadline) {
    sc_tick now = sc_tick_now();
    if (deadline <= now) {
        return false; // timeout
    }

    // Round up to the next millisecond to guarantee that the deadline is
    // reached when returning due to timeout
    uint32_t ms = SC_TICK_TO_MS(deadline - now + SC_TICK_FROM_MS(1) - 1);
    int r = SDL_CondWaitTimeout(cond->cond, mutex->mutex, ms);
#ifndef NDEBUG
    if (r < 0) {
        LOGE("Could not wait on condition with timeout: %s", SDL_GetError());
        abort();
    }

    atomic_store_explicit(&mutex->locker, sc_thread_get_id(),
                          memory_order_relaxed);
#endif
    assert(r == 0 || r == SDL_MUTEX_TIMEDOUT);
    // The deadline is reached on timeout
    assert(r != SDL_MUTEX_TIMEDOUT || sc_tick_now() >= deadline);
    return r == 0;
}

void
sc_cond_signal(sc_cond *cond) {
    int r = SDL_CondSignal(cond->cond);
#ifndef NDEBUG
    if (r) {
        LOGE("Could not signal a condition: %s", SDL_GetError());
        abort();
    }
#else
    (void) r;
#endif
}

void
sc_cond_broadcast(sc_cond *cond) {
    int r = SDL_CondBroadcast(cond->cond);
#ifndef NDEBUG
    if (r) {
        LOGE("Could not broadcast a condition: %s", SDL_GetError());
        abort();
    }
#else
    (void) r;
#endif
}
