#include "il2cpp-config.h"
#include <memory>
#include "icalls/mscorlib/System.Threading/Thread.h"
#include "il2cpp-class-internals.h"
#include "gc/GarbageCollector.h"
#include "os/Atomic.h"
#include "os/Thread.h"
#include "os/Mutex.h"
#include "os/Semaphore.h"
#include "utils/StringUtils.h"
#include "vm/Array.h"
#include "vm/Domain.h"
#include "vm/Object.h"
#include "vm/Runtime.h"
#include "vm/String.h"
#include "vm/Thread.h"
#include "vm/Exception.h"
#include "vm/String.h"
#include "vm/StackTrace.h"
#include "utils/Memory.h"
#include "utils/StringUtils.h"
#include "vm/Atomic.h"

using il2cpp::gc::GarbageCollector;

namespace il2cpp
{
namespace icalls
{
namespace mscorlib
{
namespace System
{
namespace Threading
{
#define NUM_CACHED_CULTURES 4
#define CULTURES_START_IDX 0
#define UICULTURES_START_IDX NUM_CACHED_CULTURES

    static Il2CppObject* lookup_cached_culture(Il2CppThread *thisPtr, int32_t start_idx);
    static void cache_culture(Il2CppThread *thisPtr, Il2CppObject *culture, int start_idx);


    void Thread::ClrState(Il2CppThread* thisPtr, uint32_t state)
    {
        il2cpp::vm::Thread::ClrState(thisPtr, (il2cpp::vm::ThreadState)state);
    }

    Il2CppThread * Thread::CurrentThread_internal(void)
    {
        return il2cpp::vm::Thread::Current();
    }

    int32_t Thread::GetDomainID()
    {
        return il2cpp::vm::Domain::GetCurrent()->domain_id;
    }

    uint32_t Thread::GetState(Il2CppThread * thisPtr)
    {
        il2cpp::os::FastAutoLock lock(thisPtr->GetInternalThread()->synch_cs);
        return thisPtr->GetInternalThread()->state;
    }

    bool Thread::Join_internal(Il2CppThread * thisPtr, int32_t ms, void* thread)
    {
        // Throw ThreadStateException if thread has not been started yet.
        if (il2cpp::vm::Thread::GetState(thisPtr) & vm::kThreadStateUnstarted)
            il2cpp::vm::Exception::Raise(il2cpp::vm::Exception::GetThreadStateException("Thread has not been started."));

        // Mark current thread as blocked.
        Il2CppThread* currentThread = il2cpp::vm::Thread::Current();
        SetState(currentThread, vm::kThreadStateWaitSleepJoin);

        // Join with other thread.
        il2cpp::os::Thread* osThread = (il2cpp::os::Thread*)thisPtr->GetInternalThread()->handle;
        IL2CPP_ASSERT(osThread != NULL);
        il2cpp::os::WaitStatus status = osThread->Join(ms);

        // Unblock current thread.
        ClrState(currentThread, vm::kThreadStateWaitSleepJoin);

        if (status == kWaitStatusSuccess)
            return true;

        return false;
    }

    void Thread::ResetAbort_internal()
    {
        il2cpp::vm::Thread::ResetAbort(il2cpp::vm::Thread::Current());
    }

    Il2CppString* Thread::GetName_internal(Il2CppThread* thisPtr)
    {
        il2cpp::os::FastAutoLock lock(thisPtr->GetInternalThread()->synch_cs);

        if (thisPtr->GetInternalThread()->name_len == 0)
            return NULL;

        return il2cpp::vm::String::NewUtf16(thisPtr->GetInternalThread()->name, thisPtr->GetInternalThread()->name_len);
    }

    void Thread::SetName_internal(Il2CppThread* thisPtr, Il2CppString* name)
    {
        vm::Thread::SetName(thisPtr, name);
    }

    void Thread::SetState(Il2CppThread* thisPtr, uint32_t state)
    {
        il2cpp::os::FastAutoLock lock(thisPtr->GetInternalThread()->synch_cs);
        il2cpp::vm::Thread::SetState(thisPtr, (il2cpp::vm::ThreadState)state);
    }

    void Thread::Sleep_internal(int32_t milliseconds)
    {
        Il2CppThread* thread = il2cpp::vm::Thread::Current();
        SetState(thread, vm::kThreadStateWaitSleepJoin);
        il2cpp::os::Thread::Sleep(milliseconds, true);
        ClrState(thread, vm::kThreadStateWaitSleepJoin);
    }

    void Thread::Thread_init(Il2CppThread * thisPtr)
    {
        il2cpp::vm::Thread::Setup(thisPtr);
    }

    struct StartData
    {
        Il2CppThread* m_Thread;
        Il2CppDomain* m_Domain;
        Il2CppDelegate* m_Delegate;
        Il2CppObject* m_StartArg;
        il2cpp::os::Semaphore* m_Semaphore;
    };

    static void ThreadStart(void* arg)
    {
        StartData* startData = (StartData*)arg;

        startData->m_Semaphore->Wait();

        {
            int temp = 0;
            if (!GarbageCollector::RegisterThread(&temp))
                IL2CPP_ASSERT(0 && "GarbageCollector::RegisterThread failed");

            il2cpp::vm::StackTrace::InitializeStackTracesForCurrentThread();

            il2cpp::vm::Thread::Initialize(startData->m_Thread, startData->m_Domain);
            il2cpp::vm::Thread::SetState(startData->m_Thread, vm::kThreadStateRunning);

            try
            {
                Il2CppException* exc = NULL;
                void* args[1] = { startData->m_StartArg };
                vm::Runtime::DelegateInvoke(startData->m_Delegate, args, &exc);

                if (exc)
                    vm::Runtime::UnhandledException(exc);
            }
            catch (il2cpp::vm::Thread::NativeThreadAbortException)
            {
                // Nothing to do. We've successfully aborted the thread.
                il2cpp::vm::Thread::SetState(startData->m_Thread, vm::kThreadStateAborted);
            }

            il2cpp::vm::Thread::ClrState(startData->m_Thread, vm::kThreadStateRunning);
            il2cpp::vm::Thread::SetState(startData->m_Thread, vm::kThreadStateStopped);
            il2cpp::vm::Thread::Uninitialize(startData->m_Thread);

            il2cpp::vm::StackTrace::CleanupStackTracesForCurrentThread();
        }

        delete startData->m_Semaphore;
        GarbageCollector::FreeFixed(startData);
    }

    intptr_t Thread::Thread_internal(Il2CppThread * thisPtr, Il2CppDelegate * start)
    {
        IL2CPP_ASSERT(thisPtr->GetInternalThread()->synch_cs != NULL);
        il2cpp::os::FastAutoLock lock(thisPtr->GetInternalThread()->synch_cs);

        if (il2cpp::vm::Thread::GetState(thisPtr) & vm::kThreadStateAborted)
        {
            return reinterpret_cast<intptr_t>(thisPtr->GetInternalThread()->handle);
        }

        // use fixed GC memory since we are storing managed object pointers
        StartData* startData = (StartData*)GarbageCollector::AllocateFixed(sizeof(StartData), NULL);

        startData->m_Thread = thisPtr;
        GarbageCollector::SetWriteBarrier((void**)&startData->m_Thread);
        startData->m_Domain = vm::Domain::GetCurrent();
        startData->m_Delegate = start;
        GarbageCollector::SetWriteBarrier((void**)&startData->m_Delegate);
        startData->m_StartArg = thisPtr->start_obj;
        GarbageCollector::SetWriteBarrier((void**)&startData->m_StartArg);
        startData->m_Semaphore = new il2cpp::os::Semaphore(0);

        il2cpp::os::Thread* thread = new il2cpp::os::Thread();
        thread->SetStackSize(thisPtr->GetInternalThread()->stack_size);
        thread->SetExplicitApartment(static_cast<il2cpp::os::ApartmentState>(thisPtr->GetInternalThread()->apartment_state));
        il2cpp::os::ErrorCode status = thread->Run(&ThreadStart, startData);
        if (status != il2cpp::os::kErrorCodeSuccess)
        {
            delete thread;
            return 0;
        }

#if NET_4_0
        uint32_t existingPriority = il2cpp::vm::Thread::GetPriority(thisPtr);
#endif

        thisPtr->GetInternalThread()->handle = thread;
        thisPtr->GetInternalThread()->state &= ~vm::kThreadStateUnstarted;
        thisPtr->GetInternalThread()->tid = thread->Id();
        if (!thisPtr->GetInternalThread()->managed_id)
            thisPtr->GetInternalThread()->managed_id = il2cpp::vm::Thread::GetNewManagedId();

        startData->m_Semaphore->Post(1, NULL);

#if NET_4_0
        il2cpp::vm::Thread::SetPriority(thisPtr, existingPriority);
#endif

        // this is just checked against 0 in the calling code
        return reinterpret_cast<intptr_t>(thisPtr->GetInternalThread()->handle);
    }

#if !NET_4_0
    void Thread::Thread_free_internal(Il2CppThread * thisPtr, intptr_t handle)
    {
        delete thisPtr->GetInternalThread()->synch_cs;
        thisPtr->GetInternalThread()->synch_cs = NULL;

        IL2CPP_FREE(thisPtr->GetInternalThread()->name);

        delete reinterpret_cast<il2cpp::os::Thread*>(handle);
    }

#endif

    static void cache_culture(Il2CppThread *thisPtr, Il2CppObject *culture, int start_idx)
    {
        int i;
        Il2CppObject *obj;
        int free_slot = -1;
        int same_domain_slot = -1;

        il2cpp::os::FastAutoLock lock(thisPtr->GetInternalThread()->synch_cs);

        if (!thisPtr->GetInternalThread()->cached_culture_info)
            IL2CPP_OBJECT_SETREF(thisPtr->GetInternalThread(), cached_culture_info, il2cpp::vm::Array::NewCached(il2cpp_defaults.object_class, NUM_CACHED_CULTURES * 2));

        for (i = start_idx; i < start_idx + NUM_CACHED_CULTURES; ++i)
        {
            obj = il2cpp_array_get(thisPtr->GetInternalThread()->cached_culture_info, Il2CppObject*, i);
            /* Free entry */
            if (!obj)
            {
                free_slot = i;
                /* we continue, because there may be a slot used with the same domain */
                continue;
            }
            /* Replace */
            same_domain_slot = i;
            break;
        }
        if (same_domain_slot >= 0)
            il2cpp_array_setref(thisPtr->GetInternalThread()->cached_culture_info, same_domain_slot, culture);
        else if (free_slot >= 0)
            il2cpp_array_setref(thisPtr->GetInternalThread()->cached_culture_info, free_slot, culture);
        /* we may want to replace an existing entry here, even when no suitable slot is found */
    }

    static Il2CppObject* lookup_cached_culture(Il2CppThread *thisPtr, int32_t start_idx)
    {
        Il2CppObject *res;
        int i;

        if (thisPtr->GetInternalThread()->cached_culture_info)
        {
            for (i = start_idx; i < start_idx + NUM_CACHED_CULTURES; ++i)
            {
                res = il2cpp_array_get(thisPtr->GetInternalThread()->cached_culture_info, Il2CppObject*, i);
                if (res)
                    return res;
            }
        }

        return NULL;
    }

    void Thread::FreeLocalSlotValues(int32_t slot, bool use_thread_local)
    {
        IL2CPP_NOT_IMPLEMENTED_ICALL(Thread::FreeLocalSlotValues);
    }

    mscorlib_System_Globalization_CultureInfo* Thread::GetCachedCurrentCulture(Il2CppThread* thisPtr)
    {
        return (mscorlib_System_Globalization_CultureInfo*)lookup_cached_culture(thisPtr, CULTURES_START_IDX);
    }

    void Thread::SetCachedCurrentCulture(Il2CppThread* thisPtr, Il2CppObject* culture)
    {
        cache_culture(thisPtr, culture, CULTURES_START_IDX);
    }

    mscorlib_System_Globalization_CultureInfo* Thread::GetCachedCurrentUICulture(Il2CppThread* thisPtr)
    {
        return (mscorlib_System_Globalization_CultureInfo*)lookup_cached_culture(thisPtr, UICULTURES_START_IDX);
    }

    void Thread::SetCachedCurrentUICulture(Il2CppThread* thisPtr, Il2CppObject* culture)
    {
        cache_culture(thisPtr, culture, UICULTURES_START_IDX);
    }

    static Il2CppArray* GetSerializedCulture(uint8_t*& data, uint32_t& length)
    {
        if (!data)
            return NULL;

        Il2CppArray* result = il2cpp::vm::Array::New(il2cpp_defaults.byte_class, length);
        memcpy(il2cpp_array_addr(result, uint8_t, 0), data, length);

        return result;
    }

    static void SetSerializedCulture(uint8_t*& data, uint32_t& length, Il2CppArray* culture)
    {
        if (data != NULL)
            delete data;

        int arrayLength = il2cpp::vm::Array::GetByteLength(culture);
        data = new uint8_t[arrayLength];
        length = arrayLength;
        memcpy(data, il2cpp_array_addr(culture, uint8_t, 0), arrayLength);
    }

#if !NET_4_0
    Il2CppArray* Thread::GetSerializedCurrentCulture(Il2CppThread* thisPtr)
    {
        il2cpp::os::FastAutoLock lock(thisPtr->synch_cs);
        return GetSerializedCulture(thisPtr->serialized_culture_info, thisPtr->serialized_culture_info_len);
    }

    void Thread::SetSerializedCurrentCulture(Il2CppThread* thisPtr, Il2CppArray* culture)
    {
        il2cpp::os::FastAutoLock lock(thisPtr->synch_cs);
        SetSerializedCulture(thisPtr->serialized_culture_info, thisPtr->serialized_culture_info_len, culture);
    }

    Il2CppArray* Thread::GetSerializedCurrentUICulture(Il2CppThread* thisPtr)
    {
        il2cpp::os::FastAutoLock lock(thisPtr->synch_cs);
        return GetSerializedCulture(thisPtr->serialized_ui_culture_info, thisPtr->serialized_ui_culture_info_len);
    }

    void Thread::SetSerializedCurrentUICulture(Il2CppThread* thisPtr, Il2CppArray* culture)
    {
        il2cpp::os::FastAutoLock lock(thisPtr->synch_cs);
        SetSerializedCulture(thisPtr->serialized_ui_culture_info, thisPtr->serialized_ui_culture_info_len, culture);
    }

#endif

    int32_t Thread::GetNewManagedId_internal()
    {
        return il2cpp::vm::Thread::GetNewManagedId();
    }

    void Thread::Abort_internal(Il2CppThread* thisPtr, Il2CppObject* stateInfo)
    {
        il2cpp::vm::Thread::RequestAbort(thisPtr);
    }

    Il2CppObject* Thread::GetAbortExceptionState(void* /* System.Threading.Thread */ self)
    {
        NOT_SUPPORTED_IL2CPP(Thread::GetAbortExceptionState, "Thread abortion is currently not implemented on IL2CPP; it is recommended to use safer mechanisms to terminate threads.");
        return 0;
    }

    void Thread::Interrupt_internal(Il2CppThread* thisPtr)
    {
        il2cpp::vm::Thread::RequestInterrupt(thisPtr);
    }

    void Thread::MemoryBarrier_()
    {
        il2cpp::os::Atomic::FullMemoryBarrier();
    }

    void Thread::Resume_internal(void* /* System.Threading.Thread */ self)
    {
        NOT_SUPPORTED_IL2CPP(Thread::Resume_internal, "Thread suspension is obsolete and not supported on IL2CPP.");
    }

    void Thread::SpinWait_nop()
    {
    }

    void Thread::Suspend_internal(void* /* System.Threading.Thread */ self)
    {
        NOT_SUPPORTED_IL2CPP(Thread::Suspend_internal, "Thread suspension is obsolete and not supported on IL2CPP.");
    }

// Unlike Mono we cannot just do a memory read/write of the correct type and reuse that for the
// floating-point types because we are compiling to C++ and have to account for its type conversion
// rules. For example, if we read a double as a uint64_t and return it as such, the compiler will
// perform a default conversion from uint64_t to double -- whereas what we want is to simply interpret
// the memory contents as a double.

    int8_t Thread::VolatileReadInt8(volatile void* address)
    {
        il2cpp::os::Atomic::FullMemoryBarrier();
        return *reinterpret_cast<volatile int8_t*>(address);
    }

    int16_t Thread::VolatileReadInt16(volatile void* address)
    {
        il2cpp::os::Atomic::FullMemoryBarrier();
        return *reinterpret_cast<volatile int16_t*>(address);
    }

    int32_t Thread::VolatileReadInt32(volatile void* address)
    {
        il2cpp::os::Atomic::FullMemoryBarrier();
        return *reinterpret_cast<volatile int32_t*>(address);
    }

    int64_t Thread::VolatileReadInt64(volatile void* address)
    {
        il2cpp::os::Atomic::FullMemoryBarrier();
        return *reinterpret_cast<volatile int64_t*>(address);
    }

    float Thread::VolatileReadFloat(volatile void* address)
    {
        il2cpp::os::Atomic::FullMemoryBarrier();
        return *reinterpret_cast<volatile float*>(address);
    }

    double Thread::VolatileReadDouble(volatile void* address)
    {
        il2cpp::os::Atomic::FullMemoryBarrier();
        return *reinterpret_cast<volatile double*>(address);
    }

    void* Thread::VolatileReadPtr(volatile void* address)
    {
        il2cpp::os::Atomic::FullMemoryBarrier();
        return *reinterpret_cast<void* volatile*>(address);
    }

    intptr_t Thread::VolatileReadIntPtr(volatile void* address)
    {
        return reinterpret_cast<intptr_t>(VolatileReadPtr(address));
    }

    void Thread::VolatileWriteInt8(volatile void* address, int8_t value)
    {
        *reinterpret_cast<volatile int8_t*>(address) = value;
        il2cpp::os::Atomic::FullMemoryBarrier();
    }

    void Thread::VolatileWriteInt16(volatile void* address, int16_t value)
    {
        *reinterpret_cast<volatile int16_t*>(address) = value;
        il2cpp::os::Atomic::FullMemoryBarrier();
    }

    void Thread::VolatileWriteInt32(volatile void* address, int32_t value)
    {
        *reinterpret_cast<volatile int32_t*>(address) = value;
        il2cpp::os::Atomic::FullMemoryBarrier();
    }

    void Thread::VolatileWriteInt64(volatile void* address, int64_t value)
    {
        *reinterpret_cast<volatile int64_t*>(address) = value;
        il2cpp::os::Atomic::FullMemoryBarrier();
    }

    void Thread::VolatileWriteFloat(volatile void* address, float value)
    {
        *reinterpret_cast<volatile float*>(address) = value;
        il2cpp::os::Atomic::FullMemoryBarrier();
    }

    void Thread::VolatileWriteDouble(volatile void* address, double value)
    {
        *reinterpret_cast<volatile double*>(address) = value;
        il2cpp::os::Atomic::FullMemoryBarrier();
    }

    void Thread::VolatileWritePtr(volatile void* address, void* value)
    {
        *reinterpret_cast<void* volatile*>(address) = value;
        il2cpp::os::Atomic::FullMemoryBarrier();
    }

    void Thread::VolatileWriteIntPtr(volatile void* address, intptr_t value)
    {
        VolatileWritePtr(address, reinterpret_cast<void*>(value));
    }

#if NET_4_0

    Il2CppArray* Thread::ByteArrayToCurrentDomain(Il2CppArray* arr)
    {
        // IL2CPP only has one domain, so just return the same array.
        return arr;
    }

    Il2CppArray* Thread::ByteArrayToRootDomain(Il2CppArray* arr)
    {
        // IL2CPP only has one domain, so just return the same array.
        return arr;
    }

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

    bool Thread::JoinInternal(Il2CppThread* _this, int32_t millisecondsTimeout)
    {
        return Join_internal(_this, millisecondsTimeout, NULL);
    }

    int32_t Thread::GetPriorityNative(Il2CppThread* _this)
    {
        return il2cpp::vm::Thread::GetPriority(_this);
    }

    int32_t Thread::SystemMaxStackStize()
    {
        return il2cpp::os::Thread::GetMaxStackSize();
    }

    Il2CppString* Thread::GetName_internal40(Il2CppInternalThread* thread)
    {
        il2cpp::os::FastAutoLock lock(thread->synch_cs);

        if (thread->name_len == 0)
            return NULL;

        return il2cpp::vm::String::NewUtf16(thread->name, thread->name_len);
    }

    Il2CppInternalThread* Thread::CurrentInternalThread_internal()
    {
        return CurrentThread_internal()->GetInternalThread();
    }

    int32_t Thread::GetState40(Il2CppInternalThread* thread)
    {
        il2cpp::os::FastAutoLock lock(thread->synch_cs);
        return (il2cpp::vm::ThreadState)thread->state;
    }

    void Thread::Abort_internal40(Il2CppInternalThread* thread, Il2CppObject* stateInfo)
    {
        il2cpp::vm::Thread::RequestAbort(thread);
    }

    void Thread::ClrState40(Il2CppInternalThread* thread, uint32_t clr)
    {
        il2cpp::vm::Thread::ClrState(thread, (il2cpp::vm::ThreadState)clr);
    }

    void Thread::ConstructInternalThread(Il2CppThread* _this)
    {
        os::Thread* osThread = new os::Thread();

        // Create managed object representing the current thread.

        Il2CppInternalThread* internal = (Il2CppInternalThread*)vm::Object::New(il2cpp_defaults.internal_thread_class);
        internal->state = vm::kThreadStateUnstarted;
        internal->handle = osThread;
        internal->tid = osThread->Id();
        internal->synch_cs = new il2cpp::os::FastMutex();
        internal->apartment_state = il2cpp::os::kApartmentStateUnknown;
        internal->managed_id = GetNewManagedId_internal();
        vm::Atomic::CompareExchangePointer<Il2CppInternalThread>(&_this->internal_thread, internal, NULL);
        il2cpp::gc::GarbageCollector::SetWriteBarrier((void**)&_this->internal_thread);
    }

    void Thread::GetStackTraces(Il2CppArray** threads, Il2CppArray** stack_frames)
    {
        IL2CPP_NOT_IMPLEMENTED_ICALL(Thread::GetStackTraces);
        IL2CPP_UNREACHABLE;
    }

    void Thread::InterruptInternal(Il2CppThread* _this)
    {
        Interrupt_internal(_this);
    }

    void Thread::ResetAbortNative(Il2CppObject* _this)
    {
        IL2CPP_NOT_IMPLEMENTED_ICALL(Thread::ResetAbortNative);
        IL2CPP_UNREACHABLE;
    }

    void Thread::ResumeInternal(Il2CppObject* _this)
    {
        NOT_SUPPORTED_IL2CPP(Thread::Resume_internal, "Thread suspension is obsolete and not supported on IL2CPP.");
    }

    void Thread::SetName_internal40(Il2CppInternalThread* thread, Il2CppString* name)
    {
        il2cpp::os::FastAutoLock lock(thread->synch_cs);

        // Throw if already set.
        if (thread->name_len != 0)
            il2cpp::vm::Exception::Raise(il2cpp::vm::Exception::GetInvalidOperationException("Thread name can only be set once."));

        // Store name.
        thread->name_len = utils::StringUtils::GetLength(name);
        thread->name = il2cpp::utils::StringUtils::StringDuplicate(utils::StringUtils::GetChars(name), thread->name_len);

        // Hand over to OS layer, if thread has been started already.
        if (thread->handle)
        {
            std::string utf8Name = il2cpp::utils::StringUtils::Utf16ToUtf8(thread->name);
            thread->handle->SetName(utf8Name.c_str());
        }
    }

    void Thread::SetPriorityNative(Il2CppThread* _this, int32_t priority)
    {
        vm::Thread::SetPriority(_this, priority);
    }

    void Thread::SetState40(Il2CppInternalThread* thread, uint32_t state)
    {
        vm::Thread::SetState(thread, (il2cpp::vm::ThreadState)state);
    }

    void Thread::SleepInternal(int32_t millisecondsTimeout)
    {
        Sleep_internal(millisecondsTimeout);
    }

    void Thread::SuspendInternal(Il2CppObject* _this)
    {
        NOT_SUPPORTED_IL2CPP(Thread::SuspendInternal, "Thread suspension is obsolete and not supported on IL2CPP.");
    }

#endif

#if NET_4_0
    Il2CppThread* Thread::GetCurrentThread()
    {
        return il2cpp::vm::Thread::Current();
    }

#endif
} /* namespace Threading */
} /* namespace System */
} /* namespace mscorlib */
} /* namespace icalls */
} /* namespace il2cpp */
