/*
 * Memory leak tracking support
 */

/**\ingroup rpmemlk
 * \page rpmemleakoverview RpMemLeak Plugin Overview 
 * 
 * The RpMemLeak plug-in was created to provide a quick and easy way to
 * check for memory allocation leaks.
 * In use, the developer simply attaches the plug-in and tells it to begin
 * tracing usually just after opening the RenderWare Grahpics engine. From this
 * point on, all memory allocations and deallocations will be tracked by
 * the RpMemLeak plug-in.
 * 
 * When the application reaches a point at which all memory should,
 * theoretically, have been deallocated eg: just prior to closing the
 * RenderWare engine the application tells the plug-in to stop tracing.
 * At the same time, the developer passes a callback function to the
 * plug-in. The callback function is called for any remaining allocated
 * blocks where it can perform any cleanup or reporting required.
 * 
 * NOTE: The memory allocation used by RenderWare is based on FreeLists.
 * To make effective use of the MemLeak plugin you must be aware of the
 * behaviour of freelists.  To summarize this, memory allocation is based
 * on a larger granularity than is strictly necessary, so, for example,
 * RenderWare will allocate space for multiple lights when one is
 * created on the expectation that in the future further lights will be
 * created.  Also, this memory is released more slowly than might be
 * expected.  Due to these behaviours, creating a single light, and
 * reporting memory usage before and after will indicate a leak.  This
 * is NOT a memory leak, but is a normal usage of memory allocation in
 * RenderWare.  Proceed with caution.
 *
 * Copyright (c) Criterion Software Limited
 */

/**********************************************************************
 *
 * File :     rpmemlk.c
 *
 * Abstract : Memory leak tracking support
 *
 **********************************************************************
 *
 * This file is a product of Criterion Software Ltd.
 *
 * This file is provided as is with no warranties of any kind and is
 * provided without any obligation on Criterion Software Ltd. or
 * Canon Inc. to assist in its use or modification.
 *
 * Criterion Software Ltd. will not, under any
 * circumstances, be liable for any lost revenue or other damages arising
 * from the use of this file.
 *
 * Copyright (c) 1998 Criterion Software Ltd.
 * All Rights Reserved.
 *
 * RenderWare is a trademark of Canon Inc.
 *
 ************************************************************************/

/*--- Include files ---*/

#include <stdlib.h>

#include "rpplugin.h"
#include <rpdbgerr.h>
#include "rpmemlk.h"           /* Memory leak structures and functions */

#if (!defined(DOXYGEN_SHOULD_SKIP_THIS))
static const char   rcsid[] __RWUNUSED__ =
    "@@(#)$Id: rpmemlk.c,v 1.23 2001/03/15 16:51:07 katherinet Exp $";
#endif /* (!defined(DOXYGEN_SHOULD_SKIP_THIS)) */

#define RWMEMLEAKGLOBAL(var)                                    \
    (RWPLUGINOFFSET(rpMemLeakGlobals,                           \
                    RwEngineInstance,                           \
                    memLeakModule.globalsOffset)->var)

#define oldMalloc(_size)                                        \
   ((RWMEMLEAKGLOBAL(oldMemFns).rwmalloc)((_size)))

#define oldRealloc(_ptr, _size)                                 \
   ((RWMEMLEAKGLOBAL(oldMemFns).rwrealloc)((_ptr), (_size)))

#define oldFree(_ptr)                                           \
   ((RWMEMLEAKGLOBAL(oldMemFns).rwfree)((_ptr)))

typedef struct _rpMemLeakGlobals rpMemLeakGlobals;
struct _rpMemLeakGlobals
{
    RwMemoryFunctions   oldMemFns;
    RpMemLeakList      *curList;
};

#if (defined(RWDEBUG))
long                rpMemLkStackDepth = 0;
#endif /* (defined(RWDEBUG)) */

static RwModuleInfo memLeakModule;

/************************************************************************
 *
 *      Function:       MemLeakGrowList()
 *
 *      Description:    Grows a memory logging list to accomodate additional
 *                      entries. 
 *                      Growth is exponential after initial allocation.
 *
 *      Parameters:     List to grow
 *
 *      Return Value:   Pointer to the list if it has been successfully grown.
 *
 ************************************************************************/
static void        *
MemLeakGrowList(RpMemLeakList * list)
{
    RpMemLeakBlockDescriptor *newMemBlocks;
    size_t              newBlockQuantity;

    RWFUNCTION(RWSTRING("MemLeakGrowList"));

    /* Go directly to the unchained memory allocator 
     * so we don't go recursive...! */
    if (list->memBlocks)
    {
        /* Double the size */
        newBlockQuantity = list->numAllocatedBlocks * 2;
        newMemBlocks = (RpMemLeakBlockDescriptor *)
            oldRealloc(list->memBlocks,
                       newBlockQuantity *
                       sizeof(RpMemLeakBlockDescriptor));
    }
    else
    {
        /* Start with 10 blocks (will grow exponentially) */
        newBlockQuantity = 10;
        newMemBlocks = (RpMemLeakBlockDescriptor *)
            oldMalloc(newBlockQuantity *
                      sizeof(RpMemLeakBlockDescriptor));
    }

    if (newMemBlocks)
    {
        list->memBlocks = newMemBlocks;
        list->numAllocatedBlocks = newBlockQuantity;
        RWRETURN((list));
    }

    RWRETURN((NULL));
}

/************************************************************************
 *
 *      Function:       MemLeakMalloc()
 *
 *      Description:    Logging malloc fuunction interposed into the RenderWare
 *                      allocation chain.
 *
 *      Parameters:     Size to allocate
 *
 *      Return Value:   Pointer to memory allocated on success.
 *
 ************************************************************************/
static void        *
MemLeakMalloc(size_t size)
{
    RpMemLeakList      *list = RWMEMLEAKGLOBAL(curList);

    /* First call the old function to do the memory thing */
    void               *newMem = oldMalloc(size);

    RWFUNCTION(RWSTRING("MemLeakMalloc"));

    /* Then log it if necessary */
    if (list && newMem)
    {
        /* Do we need to allocate more memory for the list */
        if (list->numBlocks >= list->numAllocatedBlocks)
        {
            if (MemLeakGrowList(list))
            {
                /* Now we have enough space */
                list->memBlocks[list->numBlocks].addr = newMem;
                list->memBlocks[list->numBlocks].size = size;
                list->numBlocks++;
            }
        }
        else
        {
            /* Else just whack it in there */
            list->memBlocks[list->numBlocks].addr = newMem;
            list->memBlocks[list->numBlocks].size = size;
            list->numBlocks++;
        }
    }

    /* Return to sender */
    RWRETURN((newMem));
}

/************************************************************************
 *
 *      Function:       MemLeakRealloc()
 *
 *      Description:    Logging realloc function interposed 
 *                      into the RenderWare allocation chain.
 *
 *      Parameters:     Memory to resize, Size to re-allocate to
 *
 *      Return Value:   Pointer to memory allocated on success.
 *
 ************************************************************************/
static void        *
MemLeakRealloc(void *mem, size_t newSize)
{
    RpMemLeakList      *list = RWMEMLEAKGLOBAL(curList);

    /* First call the old function to do the memory thing */
    void               *newMem = oldRealloc(mem, newSize);

    RWFUNCTION(RWSTRING("MemLeakRealloc"));

    /* Then log it if necessary */
    if (RWMEMLEAKGLOBAL(curList) && newMem)
    {
        RwUInt32            i;

        /* See if it's in the list, and replace it if it is */
        for (i = 0; i < list->numBlocks; i++)
        {
            if (list->memBlocks[i].addr == mem)
            {
                list->memBlocks[i].addr = newMem;
                list->memBlocks[i].size = newSize;
            }
        }
    }

    /* Return to sender */
    RWRETURN((newMem));
}

/************************************************************************
 *
 *      Function:       MemLeakCalloc()
 *
 *      Description:    Logging calloc fuunction interposed into the RenderWare
 *                      allocation chain.
 *
 *      Parameters:     Size to allocate
 *
 *      Return Value:   Pointer to memory allocated on success.
 *
 ************************************************************************/
static void        *
MemLeakCalloc(size_t numObj, size_t sizeObj)
{
    RpMemLeakList      *list = RWMEMLEAKGLOBAL(curList);

    /* First call the old function to do the memory thing */
    void               *newMem =
        RWMEMLEAKGLOBAL(oldMemFns).rwcalloc(numObj, sizeObj);
    RWFUNCTION(RWSTRING("MemLeakCalloc"));

    /* Then log it if necessary */
    if (list && newMem)
    {
        /* Do we need to allocate more memory for the list */
        if (list->numBlocks >= list->numAllocatedBlocks)
        {
            if (MemLeakGrowList(list))
            {
                /* Now we have enough space */
                list->memBlocks[list->numBlocks].addr = newMem;
                list->memBlocks[list->numBlocks].size =
                    numObj * sizeObj;
                list->numBlocks++;
            }
        }
        else
        {
            /* Else just whack it in there */
            list->memBlocks[list->numBlocks].addr = newMem;
            list->memBlocks[list->numBlocks].size = numObj * sizeObj;
            list->numBlocks++;
        }
    }

    /* Return to sender */
    RWRETURN((newMem));
}

/************************************************************************
 *
 *      Function:       MemLeakFree()
 *
 *      Description:    Logging free fuunction interposed into the RenderWare
 *                      allocation chain.
 *
 *      Parameters:     Memory to de-allocate
 *
 *      Return Value:   Nothing
 *
 ************************************************************************/
static void
MemLeakFree(void *mem)
{
    RpMemLeakList      *list = RWMEMLEAKGLOBAL(curList);

    RWFUNCTION(RWSTRING("MemLeakFree"));

    /* First call the old function to do the memory thing */
    RWMEMLEAKGLOBAL(oldMemFns).rwfree(mem);

    /* Then log it if necessary */
    if (RWMEMLEAKGLOBAL(curList))
    {
        RwUInt32            i;

        /* See if it's in the list, and remove it if it is */
        for (i = 0; i < list->numBlocks; i++)
        {
            if (list->memBlocks[i].addr == mem)
            {
                /* Copy the last element down */
                list->memBlocks[i] =
                    list->memBlocks[list->numBlocks - 1];
                list->numBlocks--;
            }
        }
    }

    RWRETURNVOID();
}

/************************************************************************
 *
 *      Function:       openMemleakPlugin()
 *
 *      Description:    Plugin initialisation function - interposes the logging
 *                      functions.
 *
 *      Parameters:     RenderWare instance, data offset and size
 *
 *      Return Value:   instance pointer on success, NULL on failure
 *
 ************************************************************************/
static void        *
openMemleakPlugin(void *instance,
                  RwInt32 offset, RwInt32 __RWUNUSED__ size)
{
    RwMemoryFunctions  *memFns;

    RWFUNCTION(RWSTRING("openMemleakPlugin"));
    RWASSERT(instance);

    /* Cache the globals offset */
    memLeakModule.globalsOffset = offset;

    /* Get the old memory functions */
    memFns = RwOsGetMemoryInterface();
    if (!memFns)
    {
        /* Failed to find the old memory interface */
        RWRETURN(NULL);
    }

    /* And cache them for chaining */
    RWMEMLEAKGLOBAL(oldMemFns) = *memFns;

    /* Install the new ones */
    memFns->rwmalloc = MemLeakMalloc;
    memFns->rwrealloc = MemLeakRealloc;
    memFns->rwcalloc = MemLeakCalloc;
    memFns->rwfree = MemLeakFree;

    /* We're not currently adding to any list */
    RWMEMLEAKGLOBAL(curList) = (RpMemLeakList *)NULL;

    /* One more instance */
    memLeakModule.numInstances++;

    RWRETURN(instance);
}

/************************************************************************
 *
 *      Function:       closeMemLeakPlugin()
 *
 *      Description:    Plugin de-initialisation function - puts things back
 *                      the way they were.
 *
 *      Parameters:     RenderWare instance, data offset and size
 *
 *      Return Value:   instance pointer on success, NULL on failure
 *
 ************************************************************************/
static void        *
closeMemLeakPlugin(void *instance,
                   RwInt32 __RWUNUSED__ offset,
                   RwInt32 __RWUNUSED__ size)
{
    RwMemoryFunctions  *memFns;

    RWFUNCTION(RWSTRING("closeMemLeakPlugin"));
    RWASSERT(instance);

    /* Re-instate the old functions */
    memFns = RwOsGetMemoryInterface();
    if (!memFns)
    {
        /* Failed to find the old memory interface 
         * - we can't shut down properly */
        RWRETURN(NULL);
    }

    /* Put 'em back */
    *memFns = RWMEMLEAKGLOBAL(oldMemFns);

    /* If we were still logging we may have memory allocated */
    if (RWMEMLEAKGLOBAL(curList))
    {
        RpMemLeakList      *list = RWMEMLEAKGLOBAL(curList);

        /* Free up any memory we allocated for this one */
        if (list->memBlocks)
        {
            RWMEMLEAKGLOBAL(oldMemFns).rwfree(list->memBlocks);
        }

        list->memBlocks = (RpMemLeakBlockDescriptor *)NULL;
        list->numBlocks = 0;
        list->numAllocatedBlocks = 0;
    }

    /* One less instance */
    memLeakModule.numInstances--;

    RWRETURN(instance);
}

/**
 * \ingroup rpmemlk
 * \ref RpMemLeakPluginAttach is used to attach the memory leak plugin 
 * to the RenderWare system to enable the tracking of memory leaks in an 
 * application. The memory leak plugin must be attached between initializing 
 * the system with RwEngineInit and opening it with RwEngineOpen.
 *
 * Note that the include file rpmemlk.h is required and must be included by 
 * an application wishing to use this plugin. The memory leak plugin library 
 * is contained in the file rpmemlk.lib
 *
 * \return TRUE on success, FALSE on failure
 *
 * \see RpMemLeakEndTrace
 * \see RpMemLeakStartTrace 
 * \see RwEngineInit
 * \see RwEngineOpen
 * \see RwEngineStart
 */
RwBool
RpMemLeakPluginAttach(void)
{
    RwInt32             offset;

    RWAPIFUNCTION(RWSTRING("RpMemLeakPluginAttach"));

    offset =
        RwEngineRegisterPlugin(sizeof(rpMemLeakGlobals),
                               rwID_MEMLEAKPLUGIN,
                               openMemleakPlugin, closeMemLeakPlugin);

    if (offset < 0)
    {
        /* Failed to attach plugin */
        RWRETURN(FALSE);
    }

    RWRETURN(TRUE);
}

/**
 * \ingroup rpmemlk
 * \ref RpMemLeakStartTrace is used to start the capture of memory
 * leak information connected with the use of RenderWare API functions.
 * Used in conjunction with \ref RpMemLeakEndTrace, this function allows
 * an application to control the capture of memory leak data so that only
 * areas of interest need to be monitored.
 * The memory leak plug-in must be attached before using this function.
 *
 * \param list  Pointer to a memory leak list that will receive the location
 * and size of all detected memory leaks. Memory for the list is allocated
 * by the plug-in as and when required to accommodate the memory leak data.
 *
 * \return Returns a pointer to the memory leak list if successful or NULL
 * if there is an error.
 *
 * \see RpMemLeakEndTrace
 * \see RpMemLeakPluginAttach 
 */
RpMemLeakList      *
RpMemLeakStartTrace(RpMemLeakList * list)
{
    RWAPIFUNCTION(RWSTRING("RpMemLeakStartTrace"));
    RWASSERT(memLeakModule.numInstances);
    RWASSERT(list);
    RWASSERT(!RWMEMLEAKGLOBAL(curList));

    if (memLeakModule.numInstances)
    {
        if (list)
        {
            if (!RWMEMLEAKGLOBAL(curList))
            {
                /* We're now logging memory in this list */
                RWMEMLEAKGLOBAL(curList) = list;

                /* So empty the list too */
                list->memBlocks = (RpMemLeakBlockDescriptor *)NULL;
                list->numBlocks = 0;
                list->numAllocatedBlocks = 0;

                RWRETURN(list);
            }

            RWERROR((E_RP_MEMLEAK_SESSIONSTARTED));
            RWRETURN((RpMemLeakList *)NULL);
        }

        RWERROR((E_RW_NULLP));
        RWRETURN((RpMemLeakList *)NULL);
    }

    RWERROR((E_RW_PLUGINNOTINIT));
    RWRETURN((RpMemLeakList *)NULL);
}

/**
 * \ingroup rpmemlk
 * \ref RpMemLeakEndTrace is used to stop the capture of memory leak
 * information connected with the use of RenderWare API functions. Used in
 * conjunction with \ref RpMemLeakStartTrace, this function allows an
 * application to control the capture of memory leak data so that only
 * areas of interest need to be monitored.
 * Stopping memory leak tracing initiates a series of callbacks, one for
 * every memory block allocated by RenderWare but not freed. The format of
 * the callback is:
 * void *(*RpMemLeakCallBack)(void *addr, size_t size);
 * where addr is the starting address of the block of memory and size is
 * its size in bytes.
 * The callback may return NULL to terminate further callbacks.
 * The memory leak plug-in must be attached before using this function.
 *
 * \param list  Pointer to the memory leak list used to hold information on
 * all unfreed memory blocks.
 * \param callback  Pointer to the callback function to apply to each memory block.
 *
 * \return Returns a pointer to the memory leak list if successful or NULL
 * if there is an error.
 *
 * \see RpMemLeakPluginAttach 
 * \see RpMemLeakStartTrace 
 */
RpMemLeakList      *
RpMemLeakEndTrace(RpMemLeakList * list, RpMemLeakCallBack callback)
{
    RWAPIFUNCTION(RWSTRING("RpMemLeakEndTrace"));
    RWASSERT(memLeakModule.numInstances);
    RWASSERT(list);
    RWASSERT(callback);

    if (memLeakModule.numInstances)
    {
        if (list && callback)
        {
            RwUInt32            i;

            /* Call the callback for each memory block 
             * that is still in the list */
            for (i = 0; i < list->numBlocks; i++)
            {
                if (!callback(list->memBlocks[i].addr,
                              list->memBlocks[i].size))
                {
                    break;
                }
            }

            /* Reset the list */
            if (list->memBlocks)
            {
                RWMEMLEAKGLOBAL(oldMemFns).rwfree(list->memBlocks);
            }

            list->memBlocks = (RpMemLeakBlockDescriptor *)NULL;
            list->numBlocks = 0;
            list->numAllocatedBlocks = 0;

            /* Now we are not logging */
            RWMEMLEAKGLOBAL(curList) = (RpMemLeakList *)NULL;

            RWRETURN(list);
        }

        RWERROR((E_RW_NULLP));
        RWRETURN((RpMemLeakList *)NULL);
    }

    RWERROR((E_RW_PLUGINNOTINIT));
    RWRETURN((RpMemLeakList *)NULL);
}
