/** 
 * @file    kDebugAlloc.cpp
 *
 * @internal
 * Copyright (C) 2012-2022 by LMI Technologies Inc.
 * Licensed under the MIT License.
 * Redistributed files must retain the above copyright notice.
 */
#include <kApi/Utils/kDebugAlloc.h>
#include <kApi/Data/kArray1.h>
#include <kApi/Data/kArrayList.h>
#include <kApi/Data/kMap.h>
#include <kApi/Data/kString.h>
#include <kApi/Threads/kLock.h>
#include <kApi/Utils/kBackTrace.h>

kBeginValueEx(k, kDebugAllocation)
    kAddField(kDebugAllocation, kPointer, data)
    kAddField(kDebugAllocation, kSize, size)
    kAddField(kDebugAllocation, k64u, index)
kEndValueEx()

kBeginClassEx(k, kDebugAlloc)
    kAddPrivateVMethod(kDebugAlloc, kObject, VRelease)
    kAddPrivateVMethod(kDebugAlloc, kAlloc, VGet)
    kAddPrivateVMethod(kDebugAlloc, kAlloc, VFree)
kEndClassEx()

kFx(kStatus) kDebugAlloc_Construct(kDebugAlloc* object, const kChar* name, kAlloc innerAlloc, kAlloc allocator)
{
    kAlloc alloc = kAlloc_Fallback(allocator);
    kStatus status; 

    kCheck(kAlloc_GetObject(alloc, kTypeOf(kDebugAlloc), object)); 

    if (!kSuccess(status = xkDebugAlloc_Init(*object, kTypeOf(kDebugAlloc), name, innerAlloc, alloc)))
    {
        kAlloc_FreeRef(alloc, object); 
    }

    return status; 
} 

kFx(kStatus) xkDebugAlloc_Init(kDebugAlloc object, kType type, const kChar* name, kAlloc innerAlloc, kAlloc alloc)
{
    kObjR(kDebugAlloc, object); 
    kStatus exception = kOK; 

    kCheck(kAlloc_Init(object, type, alloc)); 

    obj->base.canGetObject = kAlloc_CanGetObject(innerAlloc);
    obj->base.isForeign = kAlloc_IsForeign(innerAlloc);

    obj->innerAlloc = innerAlloc; 
    obj->lock = kNULL;
    obj->name[0] = 0;
    obj->allocListener.function = kNULL; 
    obj->allocListener.receiver = kNULL; 
    obj->allocated = 0;
    obj->counter = 0;
    obj->history = kNULL;
    obj->padPattern = kNULL;
    obj->candidateObjectLeaks = kNULL;
    obj->backTrace = kNULL;
    obj->backTraceDescriptions = kNULL;

    kTry
    {
        kTestArgs(!kIsNull(innerAlloc)); 

        if (!kIsNull(name))
        {
            kTest(kStrCopy(obj->name, kCountOf(obj->name), name)); 
        }

        kTest(kLock_ConstructEx(&obj->lock, xkLOCK_OPTION_PRIORITY_INHERITANCE, alloc));         
        kTest(kMap_Construct(&obj->history, kTypeOf(kPointer), kTypeOf(kDebugAllocation), 16, alloc)); 

        kTest(kMap_Construct(&obj->candidateObjectLeaks, kTypeOf(k64u), kTypeOf(kTypeName), 0, alloc)); 

        kTest(kBackTrace_Construct(&obj->backTrace, alloc)); 
        kTest(kMap_Construct(&obj->backTraceDescriptions, kTypeOf(kBackTrace), kTypeOf(kArrayList), 0, alloc)); 

        kTest(kArray1_Construct(&obj->padPattern, kTypeOf(kByte), xkDEBUG_ALLOC_PAD_SIZE, alloc)); 
        kTest(xkDebugAlloc_InitPadPattern(object)); 
    }
    kCatch(&exception)
    {
        xkDebugAlloc_VRelease(object); 
        kEndCatch(exception); 
    }

    return kOK; 
}

kFx(kStatus) xkDebugAlloc_VRelease(kDebugAlloc object)
{
    kObj(kDebugAlloc, object);  

    kCheck(kObject_Destroy(obj->lock)); 
    kCheck(kObject_Destroy(obj->history)); 
    kCheck(kObject_Destroy(obj->candidateObjectLeaks)); 
    kCheck(kObject_Destroy(obj->backTrace)); 
    kCheck(kObject_Dispose(obj->backTraceDescriptions)); 
    kCheck(kObject_Destroy(obj->padPattern)); 

    kCheck(kAlloc_VRelease(object)); 
    
    return kOK; 
}

kFx(kStatus) xkDebugAlloc_InitPadPattern(kDebugAlloc object)
{
    kObj(kDebugAlloc, object); 
    kByte patternBuffer[] = { 19, 223, 61, 239, 149, 181, 37, 101 };        //miscellaneous prime numbers
    kByte* padData = kArray1_DataT(obj->padPattern, kByte); 
    kSize padSize = kArray1_Count(obj->padPattern); 
    kSize i; 

    for (i = 0; i < padSize; ++i)
    {
        padData[i] = patternBuffer[i % kCountOf(patternBuffer)]; 
    }

    return kOK; 
}

kFx(kStatus) kDebugAlloc_Clear(kDebugAlloc object)
{
    kObj(kDebugAlloc, object); 
    kSize padSize = kArray1_Count(obj->padPattern); 

    kCheck(kLock_Enter(obj->lock)); 

    kTry
    {
        kMapItem it = kMap_First(obj->history); 

        while (!kIsNull(it))
        {
            const kDebugAllocation* allocation = kMap_ValueT(obj->history, it, kDebugAllocation); 
            kByte* allocatedMem = allocation->data - padSize; 
            
            kTest(kAlloc_Free(obj->innerAlloc, allocatedMem)); 
            obj->allocated -= allocation->size; 

            it = kMap_Next(obj->history, it); 
        }

        kTest(kMap_Clear(obj->history)); 

        obj->allocated = 0; 
        obj->counter = 0; 
    }
    kFinally
    {
        kLock_Exit(obj->lock); 
        kEndFinally(); 
    }

    return kOK; 
}

kFx(kStatus) xkDebugAlloc_VGet(kDebugAlloc object, kSize size, void* mem)
{
    kObj(kDebugAlloc, object); 
    kByte* allocatedMem = kNULL; 
    kSize padSize = kArray1_Count(obj->padPattern); 
    const kByte* padData = kArray1_DataT(obj->padPattern, kByte); 
    kBackTrace backTrace = kNULL; 
    kArrayList backTraceDescription = kNULL; 
    kArrayList traceInfo = kNULL; 
    kDebugAllocation allocation; 

    kCheck(kLock_Enter(obj->lock)); 

    kTry
    {   
        kTest(kBackTrace_Capture(obj->backTrace, 1)); 

        if (!kSuccess(kMap_FindT(obj->backTraceDescriptions, &obj->backTrace, &traceInfo)))
        {
            kTest(kObject_Clone(&backTrace, obj->backTrace, kObject_Alloc(object))); 
            kTest(kBackTrace_Describe(backTrace, &backTraceDescription, kObject_Alloc(object))); 

            kTest(kMap_AddT(obj->backTraceDescriptions, &backTrace, &backTraceDescription)); 

            traceInfo = backTraceDescription; 
            backTrace = backTraceDescription = kNULL; 
        }

        kTest(kAlloc_Get(obj->innerAlloc, size + 2*padSize, &allocatedMem)); 

        allocation.data = allocatedMem + padSize; 
        allocation.size = size; 
        allocation.index = obj->counter;          
        allocation.trace = traceInfo; 

        kItemCopy(allocatedMem, padData, padSize); 
        kItemCopy(allocatedMem + padSize + size, padData, padSize); 

        kMemSet(allocation.data, 0xCD, allocation.size);

        kTest(kMap_AddT(obj->history, &allocation.data, &allocation)); 
        
        allocatedMem = kNULL; 
        *(void**)mem = allocation.data;  

        obj->allocated += size; 
        obj->counter++; 

        if (!kIsNull(obj->allocListener.function))
        {
            obj->allocListener.function(obj->allocListener.receiver, object, &allocation); 
        }
    }
    kFinally
    {
        kAlloc_Free(obj->innerAlloc, allocatedMem);

        kObject_Destroy(backTrace); 
        kObject_Dispose(backTraceDescription); 

        kLock_Exit(obj->lock); 
        kEndFinally(); 
    }

    return kOK; 
}

kFx(kStatus) xkDebugAlloc_VFree(kDebugAlloc object, void* mem)
{
    kObj(kDebugAlloc, object); 
    kSize padSize = kArray1_Count(obj->padPattern); 
    const kByte* padData = kArray1_DataT(obj->padPattern, kByte); 

    if (!kIsNull(mem))
    {
        kCheck(kLock_Enter(obj->lock)); 

        kTry
        {    
            kMapItem item = kNULL; 
            kDebugAllocation* allocation = kNULL; 
            kByte* allocatedMem = kNULL; 
            kSize size = 0; 

            //If the assertion below is encountered, then the caller has attempted to free
            //an address that is not currently an outstanding memory allocation (often, double-free).
            if(!kSuccess(kMap_FindItemT(obj->history, &mem, &item)))
            {
                kAssert(kFALSE); 
                kThrow(kERROR_HEAP); 
            }

            allocation = kMap_ValueT(obj->history, item, kDebugAllocation);  
            allocatedMem = allocation->data - padSize; 
            size = allocation->size; 

            //If the assertions below are encountered, then heap memory corruption has occurred. 
            //(Memory immediately before or immediately after the user allocation has been modified.)
            if (!kMemEquals(allocatedMem, padData, padSize))
            {
                kAssert(kFALSE); 
                kThrow(kERROR_HEAP); 
            }
            else if (!kMemEquals(allocatedMem + padSize + size, padData, padSize))
            {
                kAssert(kFALSE); 
                kThrow(kERROR_HEAP); 
            }

            kTest(kMap_RemoveItem(obj->history, item));   

            kTest(kAlloc_Free(obj->innerAlloc, allocatedMem)); 

            obj->allocated -= size; 
        }
        kFinally
        {
            kLock_Exit(obj->lock); 
            kEndFinally(); 
        }
    }

    return kOK; 
}

kFx(k64u) kDebugAlloc_Checkpoint(kDebugAlloc object)
{
    kObj(kDebugAlloc, object); 
    k64u counter = 0; 

    kLock_Enter(obj->lock); 
    {
        counter = obj->counter; 
    }
    kLock_Exit(obj->lock); 

    return counter; 
}

kFx(kStatus) kDebugAlloc_Allocations(kDebugAlloc object, k64u since, kArrayList* history, kAlloc alloc)
{
    kObj(kDebugAlloc, object); 
    kArrayList historyOut = kNULL; 
    kMapItem it = kNULL; 

    kCheck(kLock_Enter(obj->lock)); 

    kTry
    {
        kTest(kArrayList_Construct(&historyOut, kTypeOf(kDebugAllocation), 0, alloc)); 

        it = kMap_First(obj->history); 

        while (!kIsNull(it))
        {
            const kDebugAllocation* allocation = kMap_ValueT(obj->history, it, kDebugAllocation); 
            
            if (allocation->index >= since)
            {
                kTest(kArrayList_AddT(historyOut, allocation)); 
            }

            it = kMap_Next(obj->history, it); 
        }

        *history = historyOut; 
        historyOut = kNULL; 
    }
    kFinally
    {
        kCheck(kObject_Destroy(historyOut)); 
        kCheck(kLock_Exit(obj->lock)); 

        kEndFinally(); 
    }    

    return kOK; 
}

kFx(kStatus) kDebugAlloc_DetectLeakedObjects(kDebugAlloc object, k64u since)
{
    kArrayList assemblies = kNULL; 
    kSize i; 

    kTry
    {
        kTest(kArrayList_Construct(&assemblies, kTypeOf(kAssembly), 0, kAlloc_System())); 
        kTest(kAssembly_Enumerate(assemblies)); 

        for (i = 0; i < kArrayList_Count(assemblies); ++i)
        {
            kAssembly assembly = kArrayList_AsT(assemblies, i, kObject); 

            kTest(kDebugAlloc_DetectLeakedAssemblyObjects(object, 0, assembly)); 
        }
    }
    kFinally
    {
        kDisposeRef(&assemblies); 

        kEndFinally(); 
    }

    return kOK; 
}


kFx(kStatus) kDebugAlloc_DetectLeakedAssemblyObjects(kDebugAlloc object, k64u since, kAssembly assembly)
{
    kObj(kDebugAlloc, object); 
    kSize typeCount = kAssembly_TypeCount(assembly); 
    kMapItem it = kNULL; 

    kCheck(kLock_Enter(obj->lock)); 

    kTry
    {
        it = kMap_First(obj->history); 

        while (!kIsNull(it))
        {
            const kDebugAllocation* allocation = kMap_ValueT(obj->history, it, kDebugAllocation); 
            
            if (allocation->index >= since)
            {
                kObject candidateObject = (kObject) allocation->data; 

                if ((allocation->size >= sizeof(kObjectClass)) && !kIsNull(xkObject_RawType(candidateObject)) && xkObject_RawVerifyTag(candidateObject))
                {
                    kType candidateType = xkObject_RawType(candidateObject); 
                    kSize i; 

                    //determine whether the candidate object header has a type field that matches a known 
                    //type in the given assembly (a hash set of types in the assembly would speed this up)
                    for (i = 0; i < typeCount; ++i)
                    {
                        kType type = kAssembly_TypeAt(assembly, i); 

                        if ((type == candidateType) && (allocation->size == kType_InnerSize(type)))
                        {
                            const kChar* typeName = kType_Name(type); 

                            kTest(kMap_ReplaceT(obj->candidateObjectLeaks, &allocation->index, typeName)); 
                        }
                    }
                }
            }

            it = kMap_Next(obj->history, it); 
        }       
    }
    kFinally
    {
        kCheck(kLock_Exit(obj->lock)); 

        kEndFinally(); 
    }    

    return kOK; 
}

kFx(kStatus) kDebugAlloc_LogAllocations(kDebugAlloc object, k64u since)
{
    kObj(kDebugAlloc, object); 
    kArrayList allocations = kNULL; 
    kSize i = 0; 
    kSize j = 0; 

    kTry
    {
        kTest(kDebugAlloc_Allocations(object, since, &allocations, kObject_Alloc(object))); 

        for (i = 0; i < kArrayList_Count(allocations); ++i)
        {
            const kDebugAllocation* allocation = kArrayList_AtT(allocations, i, kDebugAllocation); 
            const kSize DUMP_COUNT = 32; 
            const kSize DUMP_HEADER = 7; 
            const kSize DUMP_ITEM = 3; 
            kSize dumpCount = kMin_(DUMP_COUNT, allocation->size); 
            kText256 dumpMessage; 
            kTypeName typeName; 

            kLogf("%s leak - index: %llu, address: 0x%llX, size: %llu.", obj->name, (k64u)allocation->index, (k64u)(kSize)allocation->data, (k64u)allocation->size); 
        
            //dump header (must match header size constant above)
            kStrPrintf(dumpMessage, kCountOf(dumpMessage), "  Data:"); 

            //dump bytes (must match item size constant above)
            for (j = 0; j < dumpCount; ++j)
            {
                kStrPrintf(&dumpMessage[DUMP_HEADER + j*DUMP_ITEM], DUMP_ITEM+1, " %02X", allocation->data[j]); 
            }

            kLogf("%s ...", dumpMessage); 

            //print object type, if known
            if (kSuccess(kMap_FindT(obj->candidateObjectLeaks, &allocation->index, &typeName)))
            {
                kLogf("  Type: %s", typeName); 
            }

            //print stack trace, if known
            if (!kIsNull(allocation->trace) && (kArrayList_Count(allocation->trace) > 0))
            {
                kLogf("  Trace:");  

                for (j = 0; j < kArrayList_Count(allocation->trace); ++j)
                {
                    kString line = kArrayList_AsT(allocation->trace, j, kString); 
                    
                    kLogf("    %s", kString_Chars(line));  
                }
            }
  
            kLogf(""); 
        }       
    }
    kFinally
    {
        kCheck(kObject_Destroy(allocations)); 

        kEndFinally(); 
    }    

    return kOK; 
}

kFx(kStatus) kDebugAlloc_SetAllocListener(kDebugAlloc object, kCallbackFx function, kPointer receiver)
{
    kObj(kDebugAlloc, object); 
   
    kCheck(kLock_Enter(obj->lock)); 
    {
        obj->allocListener.function = function; 
        obj->allocListener.receiver = receiver; 
    }
    kCheck(kLock_Exit(obj->lock)); 

    return kOK; 
}

kFx(kSize) kDebugAlloc_Allocated(kDebugAlloc object)
{
    kObj(kDebugAlloc, object); 
    kSize allocated = 0; 

    kLock_Enter(obj->lock); 
    {
        allocated = obj->allocated; 
    }
    kLock_Exit(obj->lock); 

    return allocated; 
}
