#include "il2cpp-config.h"

#if !IL2CPP_THREADS_STD && IL2CPP_THREADS_WIN32 && !RUNTIME_TINY

#include "ThreadImpl.h"
#include "os/ThreadLocalValue.h"
#include "os/Time.h"
#include "utils/StringUtils.h"
#include "os/Debug.h"
#include "WindowsHelpers.h"

namespace il2cpp
{
namespace os
{
    static Event s_ThreadSleepObject;
    struct ThreadImplStartData
    {
        Thread::StartFunc m_StartFunc;
        void* m_StartArg;
        volatile DWORD* m_ThreadId;
    };

    static DWORD WINAPI ThreadStartWrapper(LPVOID arg)
    {
        ThreadImplStartData startData = *(ThreadImplStartData*)arg;
        free(arg);
        *startData.m_ThreadId = GetCurrentThreadId();
        startData.m_StartFunc(startData.m_StartArg);
        return 0;
    }

    ThreadImpl::ThreadImpl()
        : m_ThreadHandle(0), m_ThreadId(0), m_StackSize(IL2CPP_DEFAULT_STACK_SIZE), m_ApartmentState(kApartmentStateUnknown), m_Priority(kThreadPriorityNormal)
        , m_ConditionSemaphore(1)
    {
    }

    ThreadImpl::~ThreadImpl()
    {
        if (m_ThreadHandle != NULL)
            CloseHandle(m_ThreadHandle);
    }

    size_t ThreadImpl::Id()
    {
        return m_ThreadId;
    }

    void ThreadImpl::SetNameForDebugger(const char* name)
    {
        // http://msdn.microsoft.com/en-us/library/xcb2z8hs.aspx

        const DWORD MS_VC_EXCEPTION = 0x406D1388;

#pragma pack(push,8)
        typedef struct tagTHREADNAME_INFO
        {
            DWORD dwType; // Must be 0x1000.
            LPCSTR szName; // Pointer to name (in user addr space).
            DWORD dwThreadID; // Thread ID (-1=caller thread).
            DWORD dwFlags; // Reserved for future use, must be zero.
        } THREADNAME_INFO;
#pragma pack(pop)

        THREADNAME_INFO info;
        info.dwType = 0x1000;
        info.szName = name;
        info.dwThreadID = static_cast<DWORD>(Id());
        info.dwFlags = 0;

        __try
        {
            RaiseException(MS_VC_EXCEPTION, 0, sizeof(info) / sizeof(ULONG_PTR), (ULONG_PTR*)&info);
        }
        __except (EXCEPTION_EXECUTE_HANDLER)
        {
        }
    }

    typedef HRESULT (__stdcall *SETTHREADPROC) (HANDLE, PCWSTR);
    void ThreadImpl::SetName(const char* name)
    {
#if !IL2CPP_TARGET_WINRT
        SETTHREADPROC ProcSetThreadDescription;
        ProcSetThreadDescription = (SETTHREADPROC)GetProcAddress(GetModuleHandle(TEXT("kernel32")), "SetThreadDescription");
        if (ProcSetThreadDescription != NULL)
        {
            const UTF16String varName = utils::StringUtils::Utf8ToUtf16(name);
            (ProcSetThreadDescription)(m_ThreadHandle, varName.c_str());
        }
#endif

        if (Debug::IsDebuggerPresent())
            SetNameForDebugger(name);
    }

    void ThreadImpl::SetPriority(ThreadPriority priority)
    {
        if (m_ThreadHandle == NULL)
            m_Priority = priority;
        else
        {
            int ret = ::SetThreadPriority(m_ThreadHandle, priority - 2);
            IL2CPP_ASSERT(ret);
        }
    }

    ThreadPriority ThreadImpl::GetPriority()
    {
        if (m_ThreadHandle == NULL)
            return m_Priority;
        int ret = ::GetThreadPriority(m_ThreadHandle) + 2;
        IL2CPP_ASSERT(ret != THREAD_PRIORITY_ERROR_RETURN);
        return (ThreadPriority)ret;
    }

    ErrorCode ThreadImpl::Run(Thread::StartFunc func, void* arg, int64_t affinityMask)
    {
        // It might happen that func will start executing and will try to access m_ThreadId before CreateThread gets a chance to assign it.
        // Therefore m_ThreadId is assigned both by this thread and from the newly created thread (race condition could go the other way too).

        ThreadImplStartData* startData = (ThreadImplStartData*)malloc(sizeof(ThreadImplStartData));
        startData->m_StartFunc = func;
        startData->m_StartArg = arg;
        startData->m_ThreadId = &m_ThreadId;

        // Create thread.
        DWORD threadId;
        HANDLE threadHandle = ::CreateThread(NULL, m_StackSize, &ThreadStartWrapper, startData, STACK_SIZE_PARAM_IS_A_RESERVATION, &threadId);

        if (!threadHandle)
            return kErrorCodeGenFailure;

#if IL2CPP_TARGET_WINDOWS_GAMES || IL2CPP_TARGET_XBOXONE
        if (affinityMask != Thread::kThreadAffinityAll)
            SetThreadAffinityMask(threadHandle, static_cast<DWORD_PTR>(affinityMask));
#endif

        m_ThreadHandle = threadHandle;
        m_ThreadId = threadId;

        SetPriority(m_Priority);

        return kErrorCodeSuccess;
    }

    void ThreadImpl::Sleep(uint32_t ms, bool interruptible)
    {
        s_ThreadSleepObject.Wait(ms, interruptible);
    }

    void ThreadImpl::CheckForUserAPCAndHandle()
    {
        m_PendingAPCsMutex.Acquire();

        while (!m_PendingAPCs.empty())
        {
            APCRequest apcRequest = m_PendingAPCs.front();

            // Remove from list. Do before calling the function to make sure the list
            // is up to date in case the function throws.
            m_PendingAPCs.erase(m_PendingAPCs.begin());

            // Release mutex while we call the function so that we don't deadlock
            // if the function starts waiting on a thread that tries queuing an APC
            // on us.
            m_PendingAPCsMutex.Release();

            // Call function.
            apcRequest.callback(apcRequest.context);

            // Re-acquire mutex.
            m_PendingAPCsMutex.Acquire();
        }

        m_PendingAPCsMutex.Release();
    }

    void ThreadImpl::SetWaitObject(WaitObject* waitObject)
    {
        // This is an unprotected write as write acccess is restricted to the
        // current thread.
        m_CurrentWaitObject = waitObject;
    }

    void ThreadImpl::QueueUserAPC(Thread::APCFunc func, void* context)
    {
        IL2CPP_ASSERT(func != NULL);

        // Put on queue.
        {
            m_PendingAPCsMutex.Acquire();
            m_PendingAPCs.push_back(APCRequest(func, context));
            m_PendingAPCsMutex.Release();
        }

        // Interrupt an ongoing wait, only interrupt if we have an object waiting
        if (m_CurrentWaitObject.load())
        {
            m_ConditionSemaphore.Release(1);
        }
    }

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

    ThreadImpl* ThreadImpl::GetCurrentThread()
    {
        return Thread::GetCurrentThread()->m_Thread;
    }

namespace
{
    // It would be nice to always use CoGetApartmentType but it's only available on Windows 7 and later.
    // That's why we check for function at runtime and do a fallback on Windows XP.
    // CoGetApartmentType is always available in Windows Store Apps.

    typedef HRESULT (STDAPICALLTYPE * CoGetApartmentTypeFunc)(APTTYPE* type, APTTYPEQUALIFIER* qualifier);

    ApartmentState GetApartmentWindows7(CoGetApartmentTypeFunc coGetApartmentType, bool* implicit)
    {
        *implicit = false;

        APTTYPE type;
        APTTYPEQUALIFIER qualifier;
        const HRESULT hr = coGetApartmentType(&type, &qualifier);
        if (FAILED(hr))
        {
            IL2CPP_ASSERT(CO_E_NOTINITIALIZED == hr);
            return kApartmentStateUnknown;
        }

        switch (type)
        {
            case APTTYPE_STA:
            case APTTYPE_MAINSTA:
                return kApartmentStateInSTA;

            case APTTYPE_MTA:
                *implicit = (APTTYPEQUALIFIER_IMPLICIT_MTA == qualifier);
                return kApartmentStateInMTA;

            case APTTYPE_NA:
                switch (qualifier)
                {
                    case APTTYPEQUALIFIER_NA_ON_STA:
                    case APTTYPEQUALIFIER_NA_ON_MAINSTA:
                        return kApartmentStateInSTA;

                    case APTTYPEQUALIFIER_NA_ON_MTA:
                        return kApartmentStateInMTA;

                    case APTTYPEQUALIFIER_NA_ON_IMPLICIT_MTA:
                        *implicit = true;
                        return kApartmentStateInMTA;
                }
                break;
        }

        IL2CPP_ASSERT(0 && "CoGetApartmentType returned unexpected value.");
        return kApartmentStateUnknown;
    }

#if IL2CPP_TARGET_WINDOWS_DESKTOP

    ApartmentState GetApartmentWindowsXp(bool* implicit)
    {
        *implicit = false;

        IUnknown* context = nullptr;
        HRESULT hr = CoGetContextToken(reinterpret_cast<ULONG_PTR*>(&context));
        if (SUCCEEDED(hr))
        {
            IComThreadingInfo* info;
            hr = context->QueryInterface(&info);
            if (SUCCEEDED(hr))
            {
                THDTYPE type;
                hr = info->GetCurrentThreadType(&type);
                if (SUCCEEDED(hr))
                {
                    // THDTYPE_PROCESSMESSAGES means that we are in STA thread.
                    // Otherwise it's an MTA thread. We are not sure at this moment if CoInitializeEx has been called explicitly on this thread
                    // or if it has been implicitly made MTA by a CoInitialize call on another thread.
                    if (THDTYPE_PROCESSMESSAGES == type)
                        return kApartmentStateInSTA;

                    // Assume implicit. Even if it's explicit, we'll handle the case correctly by checking CoInitializeEx return value.
                    *implicit = true;
                    return kApartmentStateInMTA;
                }

                info->Release();
            }

            // No need to release context.
        }

        return kApartmentStateUnknown;
    }

    class CoGetApartmentTypeHelper
    {
    private:
        HMODULE _library;
        CoGetApartmentTypeFunc _func;

    public:
        inline CoGetApartmentTypeHelper()
        {
            _library = LoadLibraryW(L"ole32.dll");
            Assert(_library);
            _func = reinterpret_cast<CoGetApartmentTypeFunc>(GetProcAddress(_library, "CoGetApartmentType"));
        }

        inline ~CoGetApartmentTypeHelper()
        {
            FreeLibrary(_library);
        }

        inline CoGetApartmentTypeFunc GetFunc() const { return _func; }
    };

    inline ApartmentState GetApartmentImpl(bool* implicit)
    {
        static CoGetApartmentTypeHelper coGetApartmentTypeHelper;
        const CoGetApartmentTypeFunc func = coGetApartmentTypeHelper.GetFunc();
        return func ? GetApartmentWindows7(func, implicit) : GetApartmentWindowsXp(implicit);
    }

#else

    inline ApartmentState GetApartmentImpl(bool* implicit)
    {
        return GetApartmentWindows7(CoGetApartmentType, implicit);
    }

#endif
}

    ApartmentState ThreadImpl::GetApartment()
    {
        Assert(GetCurrentThreadId() == m_ThreadId);

        ApartmentState state = static_cast<ApartmentState>(m_ApartmentState & ~kApartmentStateCoInitialized);

        if (kApartmentStateUnknown == state)
        {
            bool implicit;
            state = GetApartmentImpl(&implicit);
            if (!implicit)
                m_ApartmentState = state;
        }

        return state;
    }

    ApartmentState ThreadImpl::GetExplicitApartment()
    {
        return static_cast<ApartmentState>(m_ApartmentState & ~kApartmentStateCoInitialized);
    }

    ApartmentState ThreadImpl::SetApartment(ApartmentState state)
    {
        Assert(GetCurrentThreadId() == m_ThreadId);

        // Unknown state uninitializes COM.
        if (kApartmentStateUnknown == state)
        {
            if (m_ApartmentState & kApartmentStateCoInitialized)
            {
                CoUninitialize();
                m_ApartmentState = kApartmentStateUnknown;
            }

            return GetApartment();
        }

        // Initialize apartment state. Ignore result of this function because it will return MTA value for both implicit and explicit apartment.
        // On the other hand m_ApartmentState will only be set to MTA if it was initialized explicitly with CoInitializeEx.
        GetApartment();

        ApartmentState currentState = static_cast<ApartmentState>(m_ApartmentState & ~kApartmentStateCoInitialized);

        if (kApartmentStateUnknown != currentState)
        {
            Assert(state == currentState);
            return currentState;
        }

#if IL2CPP_TARGET_XBOXONE
        if (state == kApartmentStateInSTA)
        {
            // Only assert in debug.. we wouldn't want to bring down the application in Release config
            IL2CPP_ASSERT(false && "STA apartment state is not supported on Xbox One");
            state = kApartmentStateInMTA;
        }
#endif

        HRESULT hr = CoInitializeEx(nullptr, (kApartmentStateInSTA == state) ? COINIT_APARTMENTTHREADED : COINIT_MULTITHREADED);
        if (SUCCEEDED(hr))
        {
            m_ApartmentState = state;
            if (S_OK == hr)
                m_ApartmentState = static_cast<ApartmentState>(m_ApartmentState | kApartmentStateCoInitialized);
            else
                CoUninitialize(); // Someone has already called correct CoInitialize. Don't leave incorrect reference count.
        }
        else if (RPC_E_CHANGED_MODE == hr)
        {
            // CoInitialize has already been called with a different apartment state.
            m_ApartmentState = (kApartmentStateInSTA == state) ? kApartmentStateInMTA : kApartmentStateInSTA;
        }
        else
        {
            // Based on where this function is called (Init and Shutdown) we can't really recover from this, so
            // just abort.
            abort();
        }

        return GetApartment();
    }

    void ThreadImpl::SetExplicitApartment(ApartmentState state)
    {
        Assert(!(m_ApartmentState & kApartmentStateCoInitialized));
        m_ApartmentState = state;
    }

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

    ThreadImpl* ThreadImpl::CreateForCurrentThread()
    {
        ThreadImpl* thread = new ThreadImpl();
        BOOL duplicateResult = DuplicateHandle(::GetCurrentProcess(), ::GetCurrentThread(), ::GetCurrentProcess(), &thread->m_ThreadHandle, 0, FALSE, DUPLICATE_SAME_ACCESS);
        Assert(duplicateResult && "DuplicateHandle failed.");
        thread->m_ThreadId = ::GetCurrentThreadId();
        return thread;
    }

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

#if IL2CPP_HAS_NATIVE_THREAD_CLEANUP

    static Thread::ThreadCleanupFunc s_ThreadCleanupFunction;
    static ThreadLocalValue s_ThreadCleanupArguments;

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

    void ThreadImpl::RegisterCurrentThreadForCleanup(void* arg)
    {
        s_ThreadCleanupArguments.SetValue(arg);
    }

    void ThreadImpl::UnregisterCurrentThreadForCleanup()
    {
        s_ThreadCleanupArguments.SetValue(NULL);
    }

    void ThreadImpl::OnCurrentThreadExiting()
    {
        Thread::ThreadCleanupFunc cleanupFunction = s_ThreadCleanupFunction;
        if (cleanupFunction == NULL)
            return;

        void* threadCleanupArgument = NULL;
        s_ThreadCleanupArguments.GetValue(&threadCleanupArgument);

        if (threadCleanupArgument != NULL)
            cleanupFunction(threadCleanupArgument);
    }

#endif
}
}

#endif
