#if IL2CPP_ENABLE_WRITE_BARRIER_VALIDATION

#include "gc_wrapper.h"
#include "il2cpp-config.h"
#include "il2cpp-api.h"
#include "gc/WriteBarrierValidation.h"
#include "gc/GarbageCollector.h"
#include "os/Mutex.h"
#include "vm/StackTrace.h"
#include <algorithm>
#include <functional>
#include <map>
#include <string>

// On systems which have the posix backtrace API, you can turn on this define to get native stack traces.
// This can make it easier to debug issues with missing barriers.
#define HAS_POSIX_BACKTRACE IL2CPP_TARGET_OSX
#define HAS_WINDOWS_BACKTRACE IL2CPP_TARGET_WINDOWS

#if HAS_POSIX_BACKTRACE
#include <execinfo.h>
#endif

#if HAS_WINDOWS_BACKTRACE
#include <windows.h>
#endif

using namespace il2cpp::os;
using namespace il2cpp::vm;

#if !IL2CPP_GC_BOEHM
#error "Write Barrier Validation is specific to Boehm GC"
#endif

namespace il2cpp
{
namespace gc
{
    enum AllocationKind
    {
        kPtrFree = 0,
        kNormal = 1,
        kUncollectable = 2,
        kObject = 3
    };

    struct AllocationInfo
    {
        size_t size;
        StackFrames stacktrace;
#if HAS_POSIX_BACKTRACE || HAS_WINDOWS_BACKTRACE
        void *backtraceFrames[128];
        int frameCount;
#endif
        AllocationKind kind;
    };

    static std::map<void*, AllocationInfo, std::greater<void*> > g_Allocations;
    static std::map<void**, void*> g_References;
    static void* g_MinHeap = (void*)0xffffffffffffffffULL;
    static void* g_MaxHeap = 0;
    static WriteBarrierValidation::ExternalAllocationTrackerFunction g_ExternalAllocationTrackerFunction = NULL;
    static WriteBarrierValidation::ExternalWriteBarrierTrackerFunction g_ExternalWriteBarrierTrackerFunction = NULL;
    static FastMutex s_AllocationMutex;
    static FastMutex s_WriteBarrierMutex;

    extern "C" void* GC_malloc_kind(size_t size, int k);

#if HAS_POSIX_BACKTRACE
    std::string GetStackPosix(const AllocationInfo &info)
    {
        std::string result;
        char **frameStrings = backtrace_symbols(&info.backtraceFrames[0], info.frameCount);
        int frameCount = std::min(info.frameCount, 32);
        if (frameStrings != NULL)
        {
            for (int x = 0; x < frameCount; x++)
            {
                result += frameStrings[x];
                result += "\n";
            }
            free(frameStrings);
        }
        return result;
    }

#endif

    void* GC_malloc_wrapper(size_t size, AllocationKind kind)
    {
        void* ptr = (char*)GC_malloc_kind(size, kind);
        memset(ptr, 0, size);

        if (g_ExternalAllocationTrackerFunction != NULL)
            g_ExternalAllocationTrackerFunction(ptr, size, kind);
        else
        {
            const StackFrames *trace = StackTrace::GetStackFrames();

            os::FastAutoLock lock(&s_AllocationMutex);

            AllocationInfo& allocation = g_Allocations[ptr];
            allocation.size = size;
            allocation.kind = kind;
            if (trace != NULL)
                allocation.stacktrace = *trace;
#if HAS_POSIX_BACKTRACE
            allocation.frameCount = backtrace(&allocation.backtraceFrames[0], 128);
#endif
#if HAS_WINDOWS_BACKTRACE
            allocation.frameCount = CaptureStackBackTrace(0, 128, &allocation.backtraceFrames[0], NULL);
#endif

            g_MinHeap = std::min(g_MinHeap, ptr);
            g_MaxHeap = std::max(g_MaxHeap, (void*)((char*)ptr + size));
        }

        return ptr;
    }

    extern "C" void GC_dirty_inner(void **ptr)
    {
        if (g_ExternalWriteBarrierTrackerFunction)
            g_ExternalWriteBarrierTrackerFunction(ptr);
        else
        {
            os::FastAutoLock lock(&s_WriteBarrierMutex);
            g_References[ptr] = *ptr;
        }
    }

    extern "C" void GC_free(void *ptr)
    {
    }

    extern "C" void* GC_malloc(size_t size)
    {
        return GC_malloc_wrapper(size, kNormal);
    }

    extern "C" void* GC_gcj_malloc(size_t size, void * ptr_to_struct_containing_descr)
    {
        void ** ptr = (void**)GC_malloc_wrapper(size, kObject);
        *ptr = ptr_to_struct_containing_descr;
        return ptr;
    }

    extern "C" void* GC_malloc_uncollectable(size_t size)
    {
        return GC_malloc_wrapper(size, kUncollectable);
    }

    extern "C" void* GC_malloc_atomic(size_t size)
    {
        return GC_malloc_wrapper(size, kPtrFree);
    }

    static std::string ObjectName(void* object, AllocationKind kind)
    {
        if (kind != kObject)
            return "?";

        Il2CppClass* klass = il2cpp_object_get_class((Il2CppObject*)(object));

        if (klass == NULL)
            return "";

        std::string name = il2cpp_class_get_name(klass);
        Il2CppClass* parent = il2cpp_class_get_declaring_type(klass);
        while (parent != NULL)
        {
            klass = parent;
            parent = il2cpp_class_get_declaring_type(klass);
            name = std::string(il2cpp_class_get_name(klass)) + "/" + name;
        }

        return std::string(il2cpp_class_get_namespace(klass)) + "::" + name;
    }

    static std::string GetReadableStackTrace(const StackFrames &stackTrace)
    {
        std::string str;
        for (StackFrames::const_iterator i = stackTrace.begin(); i != stackTrace.end(); i++)
        {
            Il2CppClass* parent = il2cpp_method_get_declaring_type(i->method);
            str += il2cpp_class_get_namespace(parent);
            str += '.';
            str += il2cpp_class_get_name(parent);
            str += ':';
            str += il2cpp_method_get_name(i->method);
            str += '\n';
        }
        return str;
    }

    static std::string LogError(std::pair<void*, AllocationInfo> const & object, void** reference, void *refObject)
    {
        std::string msg;
        char chbuf[1024];
        snprintf(chbuf, 1024, "In object %p (%s) with size %zx at offset %zx, allocated at \n%s\n", object.first, ObjectName(object.first, object.second.kind).c_str(), object.second.size, (size_t)((char*)reference - (char*)object.first),
#if HAS_POSIX_BACKTRACE
            GetStackPosix(object.second).c_str()
#else
            GetReadableStackTrace(object.second.stacktrace).c_str()
#endif
        );
        msg += chbuf;
        snprintf(chbuf, 1024, "Points to object %p of type (%s)\n", refObject, ObjectName(refObject, kPtrFree).c_str());
        msg += chbuf;
        return msg;
    }

    // Boehm internal constants
    #define GC_DS_TAG_BITS 2
    #define GC_DS_TAGS   ((1 << GC_DS_TAG_BITS) - 1)
    #define GC_DS_LENGTH 0  /* The entire word is a length in bytes that    */
    /* must be a multiple of 4.                     */
    #define GC_DS_BITMAP 1  /* 30 (62) bits are a bitmap describing pointer */

    #ifndef MARK_DESCR_OFFSET
    #  define MARK_DESCR_OFFSET sizeof(void*)
    #endif

    void WriteBarrierValidation::SetExternalAllocationTracker(ExternalAllocationTrackerFunction func)
    {
        g_ExternalAllocationTrackerFunction = func;
    }

    void WriteBarrierValidation::SetExternalWriteBarrierTracker(ExternalWriteBarrierTrackerFunction func)
    {
        g_ExternalWriteBarrierTrackerFunction = func;
    }

    void WriteBarrierValidation::Setup()
    {
        GarbageCollector::Disable();
    }

    void WriteBarrierValidation::Run()
    {
        if (g_ExternalAllocationTrackerFunction != NULL)
            return;
        std::string msg;
        msg = "<TestResult Name='WriteBarrierValidation'>\n<![CDATA[\n";
        size_t errors = 0;
        for (std::map<void*, AllocationInfo>::iterator i = g_Allocations.begin(); i != g_Allocations.end(); i++)
        {
            if (i->second.kind == kPtrFree)
                continue;

            intptr_t desc = (intptr_t)GC_NO_DESCRIPTOR;
            if (i->second.kind == kObject)
            {
                desc = *(intptr_t*)(((char*)(*(void**)i->first)) + MARK_DESCR_OFFSET);
                if ((desc & GC_DS_TAGS) != GC_DS_BITMAP)
                    desc = (intptr_t)GC_NO_DESCRIPTOR;
            }

            for (void** ptr = ((void**)i->first) + 2; ptr < (void**)((char*)i->first + i->second.size); ptr++)
            {
                if (desc != (intptr_t)GC_NO_DESCRIPTOR)
                {
                    // if we have a GC descriptor bitmap, check if this address can be a pointer, skip otherwise.
                    size_t ptr_index = ptr - (void**)i->first;
                    if (((desc >> (sizeof(void*) * 8 - ptr_index - 1)) & 1) == 0)
                        continue;
                }

                void* ref = *ptr;
                if (ref < g_MinHeap || ref >= g_MaxHeap)
                    continue;

                std::map<void*, AllocationInfo>::iterator j = g_Allocations.lower_bound(ref);
                if (j != g_Allocations.end() && j != i && j->second.kind != kUncollectable)
                {
                    if (j->first <= ref && (void*)((char*)j->first + j->second.size) > ref)
                    {
                        std::map<void**, void*>::iterator trackedRef = g_References.find(ptr);
                        if (trackedRef == g_References.end())
                        {
                            char chbuf[1024];
                            snprintf(chbuf, 1024, "%p looks like a reference to %p, but was not found in tracked references\n", ptr, ref);
                            msg += chbuf;
                            errors++;
                            msg += LogError(*i, ptr, j->first);
                        }
                        else if (trackedRef->second != ref)
                        {
                            char chbuf[1024];
                            snprintf(chbuf, 1024, "%p does not match tracked value (%p!=%p).\n", ptr, trackedRef->second, ref);
                            msg += chbuf;
                            errors++;
                            msg += LogError(*i, ptr, j->first);
                        }
                    }
                }
            }
        }
        msg += "]]>\n</TestResult>\n";
        if (errors > 0)
            printf("%s", msg.c_str());
    }
} /* gc */
} /* il2cpp */

#endif
