#include "os/c-api/il2cpp-config-platforms.h"
#include "os/Thread.h"

#if IL2CPP_SUPPORT_THREADS

#include "os/Mutex.h"
#include "os/ThreadLocalValue.h"
#if IL2CPP_THREADS_STD
#include "os/Std/ThreadImpl.h"
#elif IL2CPP_TARGET_WINDOWS
#include "os/Win32/ThreadImpl.h"
#elif IL2CPP_THREADS_PTHREAD
#include "os/Posix/ThreadImpl.h"
#else
#include "os/ThreadImpl.h"
#endif

#include "utils/dynamic_array.h"

#include "Baselib.h"
#include "Cpp/ReentrantLock.h"

#include <limits>

namespace il2cpp
{
namespace os
{
/// TLS variable referring to current thread.
    static ThreadLocalValue s_CurrentThread;

    // TLS variable referring to whether this thread is currently executing Thread::Shutdown
    // It is thread local for thread safety
    static ThreadLocalValue s_IsCleaningUpThreads;

    struct ThreadContext
    {
        baselib::ReentrantLock m_AliveThreadsMutex;
        il2cpp::utils::dynamic_array<Thread*> m_AliveThreads;
    };

    ThreadContext* s_ThreadContext = nullptr;

    int64_t Thread::s_DefaultAffinityMask = kThreadAffinityAll;

    static bool GetIsCleaningUpThreads()
    {
        void* value = NULL;
        s_IsCleaningUpThreads.GetValue(&value);
        return reinterpret_cast<intptr_t>(value) != 0;
    }

    static void SetIsCleaningUpThreads(bool value)
    {
        s_IsCleaningUpThreads.SetValue(reinterpret_cast<void*>(static_cast<intptr_t>(value)));
    }

    Thread::Thread()
        : m_Thread(new ThreadImpl())
        , m_State(kThreadCreated)
        , m_ThreadExitedEvent(true) // Manual reset event
        , m_CleanupFunc(NULL)
        , m_CleanupFuncArg(NULL)
    {
        FastAutoLock lock(&s_ThreadContext->m_AliveThreadsMutex);
        s_ThreadContext->m_AliveThreads.push_back(this);
    }

    Thread::Thread(ThreadImpl* thread)
        : m_Thread(thread)
        , m_State(kThreadRunning)
        , m_ThreadExitedEvent(true) // Manual reset event
        , m_CleanupFunc(NULL)
        , m_CleanupFuncArg(NULL)
    {
        FastAutoLock lock(&s_ThreadContext->m_AliveThreadsMutex);
        s_ThreadContext->m_AliveThreads.push_back(this);
    }

    Thread::~Thread()
    {
        delete m_Thread;

        if (!GetIsCleaningUpThreads())
        {
            FastAutoLock lock(&s_ThreadContext->m_AliveThreadsMutex);
            size_t count = s_ThreadContext->m_AliveThreads.size();
            for (size_t i = 0; i < count; i++)
            {
                if (s_ThreadContext->m_AliveThreads[i] == this)
                {
                    s_ThreadContext->m_AliveThreads.erase_swap_back(&s_ThreadContext->m_AliveThreads[i]);
                    break;
                }
            }
        }
    }

    void Thread::Init()
    {
        il2cpp::os::ThreadImpl::AllocateStaticData();

        s_ThreadContext = new ThreadContext();

        Thread* thread = GetOrCreateCurrentThread();
        if (thread->GetApartment() == kApartmentStateUnknown)
            thread->SetApartment(kApartmentStateInMTA);
    }

    void Thread::Shutdown()
    {
        Thread* currentThread = GetCurrentThread();
        currentThread->SetApartment(kApartmentStateUnknown);

        SetIsCleaningUpThreads(true);

        {
            FastAutoLock lock(&s_ThreadContext->m_AliveThreadsMutex);
            size_t count = s_ThreadContext->m_AliveThreads.size();
            for (size_t i = 0; i < count; i++)
            {
                // If this is not the current thread, wait a bit for it to exit. This will avoid an
                // infinite wait on shutdown, but it should give the thread enough time to complete its
                // use of the os::Thread object before we delete it. Note that we don't call Join here,
                // as we want to explicitly do a non-interruptable wait because we are pretty late in
                // the shutdown process. The VM thread code should have already caused any running
                // threads to get a thread abort exception, meaning that any running OS threads will
                // be exiting soon, with no need to check for APCs.
                if (s_ThreadContext->m_AliveThreads[i] != currentThread)
                {
                    s_ThreadContext->m_AliveThreads[i]->m_ThreadExitedEvent.Wait(10, false);
                    delete s_ThreadContext->m_AliveThreads[i];
                }
            }

            // Wait to delete the current thread last, as waiting on an event may need to access the current thread
            delete currentThread;

            s_ThreadContext->m_AliveThreads.clear();
        }

        SetIsCleaningUpThreads(false);
#if IL2CPP_ENABLE_RELOAD
        s_CurrentThread.SetValue(NULL);
#endif
        delete s_ThreadContext;
        s_ThreadContext = nullptr;

        il2cpp::os::ThreadImpl::FreeStaticData();
    }

    Thread::ThreadId Thread::Id()
    {
        return m_Thread->Id();
    }

    void Thread::SetName(const char* name)
    {
        m_Thread->SetName(name);
    }

    void Thread::SetPriority(ThreadPriority priority)
    {
        m_Thread->SetPriority(priority);
    }

    ThreadPriority Thread::GetPriority()
    {
        return m_Thread->GetPriority();
    }

    void Thread::SetStackSize(size_t stackSize)
    {
        m_Thread->SetStackSize(stackSize);
    }

    int Thread::GetMaxStackSize()
    {
        return ThreadImpl::GetMaxStackSize();
    }

    struct StartData
    {
        Thread* thread;
        Thread::StartFunc startFunction;
        void* startFunctionArgument;
    };

/// Wrapper for the user's thread start function. Sets s_CurrentThread.
    void Thread::RunWrapper(void* arg)
    {
        StartData* data = reinterpret_cast<StartData*>(arg);

        // Store thread reference.
        Thread* thread = data->thread;

        const ApartmentState apartment = thread->GetExplicitApartment();
        if (apartment != kApartmentStateUnknown)
        {
            thread->SetExplicitApartment(kApartmentStateUnknown);
            thread->SetApartment(apartment);
        }

        s_CurrentThread.SetValue(thread);

        // Get rid of StartData.
        StartFunc startFunction = data->startFunction;
        void* startFunctionArgument = data->startFunctionArgument;
        delete data;

        // Make sure thread exit event is not signaled.
        thread->m_ThreadExitedEvent.Reset();

        // Run user thread start function.
        thread->m_State = kThreadRunning;
        startFunction(startFunctionArgument);
        thread->m_State = kThreadExited;

        thread->SetApartment(kApartmentStateUnknown);

        CleanupFunc cleanupFunc = thread->m_CleanupFunc;
        void* cleanupFuncArg = thread->m_CleanupFuncArg;

        // Signal that we've finished execution.
        thread->m_ThreadExitedEvent.Set();

        if (cleanupFunc)
            cleanupFunc(cleanupFuncArg);
    }

    ErrorCode Thread::Run(StartFunc func, void* arg)
    {
        IL2CPP_ASSERT(m_State == kThreadCreated || m_State == kThreadExited);

        StartData* startData = new StartData;
        startData->startFunction = func;
        startData->startFunctionArgument = arg;
        startData->thread = this;

        return m_Thread->Run(RunWrapper, startData, s_DefaultAffinityMask);
    }

    WaitStatus Thread::Join()
    {
        IL2CPP_ASSERT(this != GetCurrentThread() && "Trying to join the current thread will deadlock");
        return Join(std::numeric_limits<uint32_t>::max());
    }

    WaitStatus Thread::Join(uint32_t ms)
    {
        // Wait for thread exit event.
        if (m_ThreadExitedEvent.Wait(ms, true) != kWaitStatusSuccess)
            return kWaitStatusFailure;

        return kWaitStatusSuccess;
    }

    void Thread::QueueUserAPC(APCFunc func, void* context)
    {
        m_Thread->QueueUserAPC(func, context);
    }

    ApartmentState Thread::GetApartment()
    {
#if IL2CPP_THREAD_IMPL_HAS_COM_APARTMENTS
        return m_Thread->GetApartment();
#else
        return kApartmentStateUnknown;
#endif
    }

    ApartmentState Thread::GetExplicitApartment()
    {
#if IL2CPP_THREAD_IMPL_HAS_COM_APARTMENTS
        return m_Thread->GetExplicitApartment();
#else
        return kApartmentStateUnknown;
#endif
    }

    ApartmentState Thread::SetApartment(ApartmentState state)
    {
#if IL2CPP_THREAD_IMPL_HAS_COM_APARTMENTS
        return m_Thread->SetApartment(state);
#else
        NO_UNUSED_WARNING(state);
        return GetApartment();
#endif
    }

    void Thread::SetExplicitApartment(ApartmentState state)
    {
#if IL2CPP_THREAD_IMPL_HAS_COM_APARTMENTS
        m_Thread->SetExplicitApartment(state);
#else
        NO_UNUSED_WARNING(state);
#endif
    }

    void Thread::Sleep(uint32_t milliseconds, bool interruptible)
    {
        ThreadImpl::Sleep(milliseconds, interruptible);
    }

    size_t Thread::CurrentThreadId()
    {
        return ThreadImpl::CurrentThreadId();
    }

    Thread* Thread::GetCurrentThread()
    {
        void* value;
        s_CurrentThread.GetValue(&value);
        IL2CPP_ASSERT(value != NULL);
        return reinterpret_cast<Thread*>(value);
    }

    bool Thread::HasCurrentThread()
    {
        void* value;
        s_CurrentThread.GetValue(&value);
        return value != NULL;
    }

    Thread* Thread::GetOrCreateCurrentThread()
    {
        Thread* thread = NULL;
        s_CurrentThread.GetValue(reinterpret_cast<void**>(&thread));
        if (thread)
            return thread;

        // The os::Thread object is deallocated in the InternalThread::Thread_free_internal icall, which
        // is called from the managed thread finalizer.
        thread = new Thread(ThreadImpl::CreateForCurrentThread());
        s_CurrentThread.SetValue(thread);

        return thread;
    }

    void Thread::DetachCurrentThread()
    {
        // PTHREAD cleanup isn't deterministic: it could be that our thread local variables get cleaned up before thread clean up routine runs
#if IL2CPP_DEBUG && !IL2CPP_THREADS_PTHREAD
        void* value;
        s_CurrentThread.GetValue(&value);
        IL2CPP_ASSERT(value != NULL);
#endif

        s_CurrentThread.SetValue(NULL);
    }

    bool Thread::YieldInternal()
    {
        return ThreadImpl::YieldInternal();
    }

    void Thread::SetDefaultAffinityMask(int64_t affinityMask)
    {
        s_DefaultAffinityMask = affinityMask;
    }

#if IL2CPP_HAS_NATIVE_THREAD_CLEANUP

    void Thread::SetNativeThreadCleanup(ThreadCleanupFunc cleanupFunction)
    {
        ThreadImpl::SetNativeThreadCleanup(cleanupFunction);
    }

    void Thread::RegisterCurrentThreadForCleanup(void* arg)
    {
        ThreadImpl::RegisterCurrentThreadForCleanup(arg);
    }

    void Thread::UnregisterCurrentThreadForCleanup()
    {
        ThreadImpl::UnregisterCurrentThreadForCleanup();
    }

    void Thread::SignalExited()
    {
        m_ThreadExitedEvent.Set();
    }

#endif
}
}

#else

#include <limits.h>

namespace il2cpp
{
namespace os
{
    int64_t Thread::s_DefaultAffinityMask = -1;

    Thread::Thread()
    {
    }

    Thread::~Thread()
    {
    }

    void Thread::Init()
    {
    }

    void Thread::Shutdown()
    {
    }

    Thread::ThreadId Thread::Id()
    {
        return 0;
    }

    void Thread::SetName(const char* name)
    {
    }

    void Thread::SetPriority(ThreadPriority priority)
    {
    }

    ThreadPriority Thread::GetPriority()
    {
        return kThreadPriorityLowest;
    }

    void Thread::SetStackSize(size_t stackSize)
    {
    }

    int Thread::GetMaxStackSize()
    {
        return INT_MAX;
    }

    void Thread::RunWrapper(void* arg)
    {
    }

    ErrorCode Thread::Run(StartFunc func, void* arg)
    {
        IL2CPP_ASSERT(0 && "Threads are not enabled for this platform.");
        return kErrorCodeSuccess;
    }

    WaitStatus Thread::Join()
    {
        IL2CPP_ASSERT(0 && "Threads are not enabled for this platform.");
        return kWaitStatusSuccess;
    }

    WaitStatus Thread::Join(uint32_t ms)
    {
        IL2CPP_ASSERT(0 && "Threads are not enabled for this platform.");
        return kWaitStatusSuccess;
    }

    void Thread::QueueUserAPC(APCFunc func, void* context)
    {
    }

    ApartmentState Thread::GetApartment()
    {
        return kApartmentStateUnknown;
    }

    ApartmentState Thread::GetExplicitApartment()
    {
        return kApartmentStateUnknown;
    }

    ApartmentState Thread::SetApartment(ApartmentState state)
    {
        return kApartmentStateUnknown;
    }

    void Thread::SetExplicitApartment(ApartmentState state)
    {
    }

    void Thread::Sleep(uint32_t milliseconds, bool interruptible)
    {
    }

    size_t Thread::CurrentThreadId()
    {
        return 0;
    }

    Thread* Thread::GetCurrentThread()
    {
        return NULL;
    }

    Thread* Thread::GetOrCreateCurrentThread()
    {
        return NULL;
    }

    void Thread::DetachCurrentThread()
    {
    }

    bool Thread::YieldInternal()
    {
        return false;
    }

    void Thread::SetDefaultAffinityMask(int64_t affinityMask)
    {
        s_DefaultAffinityMask = affinityMask;
    }

#if IL2CPP_HAS_NATIVE_THREAD_CLEANUP

    void Thread::SetNativeThreadCleanup(ThreadCleanupFunc cleanupFunction)
    {
    }

    void Thread::RegisterCurrentThreadForCleanup(void* arg)
    {
    }

    void Thread::UnregisterCurrentThreadForCleanup()
    {
    }

    void Thread::SignalExited()
    {
    }

#endif
}
}

#endif
