/* $Id$ 
 * 
 * Licensed to the Apache Software Foundation (ASF) under one or more 
 * contributor license agreements. See the NOTICE file distributed with  
 * this work for additional information regarding copyright ownership. 
 * The ASF licenses this file to you under the Apache License, Version  
 * 2.0 (the "License"); you may not use this file except in compliance  
 * with the License. You may obtain a copy of the License at 
 * 
 * http://www.apache.org/licenses/LICENSE-2.0 
 * 
 * Unless required by applicable law or agreed to in writing, software 
 * distributed under the License is distributed on an "AS IS" BASIS, 
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and 
 * limitations under the License. 
 */ 

/*
 * etchmem.c -- heap memory allocate and free.
 * the c binding wraps the heap allocator in order to track allocations. 
 * we supply the etch_malloc macro which, when ETCH_DEBUGALLOC is defined, 
 * will accept module name and code line number, along with object type and
 * allocation byte length, in order to track allocations and frees, and thus 
 * enable identification of memory leaks. 
 */

#include "etch_global.h"
#include "etchlog.h"
char* ETCHMEMX = "MEMX";

#ifdef ETCH_DEBUGALLOC  /* defined, or not, in etchmem.h */

#pragma warning (disable:4313) /* display of ptr as %08x warning */

memrec* debug_find_memrec(void* p, char* file, const short line);


/*
 * find_module_name()
 * private method which looks up a code module name and if not present,
 * adds it to the filepath cache. a hash of the code file name is returned, 
 * or zero if error.
 */
unsigned find_module_name(char* name)  /* __FILE__ is not unicode */
{
    unsigned hash = 0;
    char* namefound = NULL;
    etch_hashitem  hashbucket; 
    etch_hashitem* thisitem = &hashbucket;
    memset(thisitem, 0, sizeof(etch_hashitem));

    /* look up name in cache. NULL is returned because the value is the key */
    cache_findx(name, &thisitem);
    if (thisitem->key) return thisitem->hash;

    /* insert name into cache. no value needed, name key is value. */
    hash = cache_insertx (name, NULL, FALSE);
    return hash;
}


/*
 * instantiate_memtable()
 * create the singleton memory allocation tracking hashtable. 
 */
void instantiate_memtable()
{
    is_memtable_instance = TRUE; /* flag to not debug_malloc() this allocation */
    memtable = new_hashtable(DEFETCHHEAPTABLESIZE);
    is_memtable_instance = FALSE;

    memtable->is_readonly_keys  = memtable->is_readonly_values = FALSE;
    memtable->is_tracked_memory = FALSE;
}


/*
 * showentry()
 * display contents of a memory allocation table entry. 
 * free the table entry if requested.
 */
void showentry(void** pkey, memrec* memtblentry, 
    const int is_freeitem, const int is_console)
{
    char* filepath = NULL;
    etch_hashitem  hashbucket; 
    etch_hashitem* thisitem = &hashbucket;
    memset(thisitem, 0, sizeof(etch_hashitem));

    /* look up key by its hash value. value of key is a source file path */
    cache_find_by_hash(memtblentry->filehash, &thisitem);
    filepath = thisitem->key;

    if (NULL == filepath)
        if (is_console)
            etchlog (ETCHMEMX, ETCHLOG_ERROR, 
                "etch cache error - no path with hashkey %u cached\n", 
                 memtblentry->filehash);
            /* printf("\nMEMX etch cache error - no path with hashkey %u cached\n", 
                memtblentry->filehash); 
             */
        else;
    else
    if (is_console)
        #if IS_USING_ALLOC_ID
        etchlog (ETCHMEMX, ETCHLOG_DEBUG, 
           "%s line %d serial %d size %d at %08x\n", filepath, 
            memtblentry->line, memtblentry->id, memtblentry->size, *pkey);
        /* printf("\n%s line %d serial %d size %d at %08x\n", filepath, 
               memtblentry->line, memtblentry->id, memtblentry->size, *pkey);
         */
        #else  /* IS_USING_ALLOC_ID */
         etchlog (ETCHMEMX, ETCHLOG_DEBUG, 
              "%s line %d obj %d size %d at %08x\n", filepath, 
               memtblentry->line, memtblentry->objtype, memtblentry->size, *pkey);
        /* printf("\n%s line %d obj %d size %d at %08x\n", filepath, 
               memtblentry->line, memtblentry->objtype, memtblentry->size, *pkey);
         */
        #endif /* IS_USING_ALLOC_ID */

    /* use is_freeitem with care, what we are saying here is show me what remains
     * allocated, and if there are allocations remaining, clean them up, i.e. they
     * are memory leaks. both the tracked etch_malloc allocation, and its memtable
     * bucket, are freed when this is the case.
     */
    if (is_freeitem)  
    {
        etch_free(*pkey);
    }
}


/*
 * debug_showmem()
 * show debug_malloc()'ed heap allocations outstanding. 
 * returns number of bytes currently allocated. is_freeitem specifies that we
 * should free each allocation remaining. is_console asks for stdout display
 * of statistics including source file and line number of each allocation.
 */
int debug_showmem(const int is_freeitem, const int is_console)
{
    int leaks = 0, count = 0;
    size_t allocated_bytes;
    etch_hashitem  hashbucket;
    etch_hashitem* thisentry = &hashbucket;
    if (!memtable) return 0; 
    allocated_bytes = etchheap_currbytes;

    if (is_console)
    {
       etchlog (ETCHMEMX, ETCHLOG_DEBUG,
           "etch heap hiwater count %u bytes %u\n", etchheap_hiwater, etchheap_maxbytes);
        etchlog (ETCHMEMX, ETCHLOG_DEBUG, 
           "%d bytes remain allocated on etch heap\n", allocated_bytes);
    }

    count = memtable->vtab->count(memtable->realtable, 0, 0);
    if  (0 == count) return 0;
                                      
    if  (0 == memtable->vtab->first(memtable->realtable, NULL, &thisentry))   
    {     
         showentry((void**)thisentry->key, (memrec*)thisentry->value, 
                    is_freeitem, is_console);
         leaks = 1;
    }
    else 
    if (is_console) 
        etchlog (ETCHMEMX, ETCHLOG_ERROR, "etch hashtable.first() failed\n");
                                     
    while(0 == memtable->vtab->next(memtable->realtable, NULL, &thisentry))          
    {      
          showentry((void**)thisentry->key, (memrec*)thisentry->value, 
                     is_freeitem, is_console);
          leaks++;
    }

    if (is_console)
    {
       if  (is_freeitem)
             etchlog (ETCHMEMX, ETCHLOG_ERROR,
                "%d etch heap allocations leaked %d bytes of which %d remain\n",
                 leaks, allocated_bytes, etchheap_currbytes); 
        else etchlog (ETCHMEMX, ETCHLOG_ERROR,
                "%d etch heap allocations totaling %d bytes remain\n", 
                 leaks, allocated_bytes);
    }      

    return (int)etchheap_currbytes;
}


/*
 * debug_dumpmem()
 * list contents of tracking table 
 */
void debug_dumpmem()
{
   etch_hashitem  hashbucket;
   etch_hashitem* myentry = &hashbucket;
   if (!memtable) return;
   #pragma warning (disable:4311)

    if  (0 == memtable->vtab->first(memtable->realtable, NULL, &myentry))   
         etchlog (ETCHMEMX, ETCHLOG_DEBUG,
            "%x: %x\n", *(unsigned*)myentry->key, (unsigned)myentry->value);
    else etchlog (ETCHMEMX, ETCHLOG_ERROR,"etch memtable.first() failed\n");  
        
    /*
    if  (0 == memtable->vtab->first(memtable->realtable, NULL, &myentry))   
         printf("\n%x: %x\n", *(unsigned*)myentry->key, (unsigned)myentry->value);
    else printf("\nMEMX etch memtable.first() failed\n");  
    */
     
    while(0 == memtable->vtab->next(memtable->realtable, NULL, &myentry))   
         etchlog (ETCHMEMX, ETCHLOG_DEBUG, 
            "%x: %x\n", *(unsigned*)myentry->key, (unsigned)myentry->value);      
         /* printf("%x: %x\n", *(unsigned*)myentry->key, (unsigned)myentry->value); */
}


/*
 * next_alloc_id: return memory allocation serial number 
 */
unsigned next_alloc_id() { return ++curr_alloc_id; }



/**
 *  debug_malloc()
 *  tracks mallocs by entering the malloc'ed object info, code file, and code line,
 *  into a tracking table. This is a private method -- it should not be invoked
 *  directly, but rather via the etch_malloc macro, when ETCH_DEBUGALLOC is defined.
 */
void* debug_malloc(size_t nbytes, const short objtype, char* file, const short line)
{                                       
    char *pkey = 0, breakpoint = 0;  /* __FILE__ is not unicode */
    int   hashresult  = 0;
    unsigned pathhash = 0;
    memrec* rec = 0;
    void* p = NULL; 
    if (nbytes <= 0) return NULL; 
    p = malloc(nbytes);  /* complete user's malloc request at least */

    pathhash = find_module_name(file);  /* cache source file name */
    if (pathhash == 0) return p; 

    if (!memtable)  /* lazy load the tracking table */  
        instantiate_memtable();
    
    rec = malloc(sizeof(memrec));  /* don't track tracking object allocations */
    rec->size     = nbytes;
    rec->objtype  = objtype;
    rec->line     = line;
    rec->filehash = pathhash;

    /* we can watch for a specific allocation sequence number and break into the
     * debugger here when it occurs. after an execution we would call debug_showmem()
     * to display leaked allocations and their sequence numbers. all the etch unit
     * tests do so after each test, see etch_showmem(). on a subsequent execution,
     * we would compile in a set of the global etch_watch_id to that sequence number, 
     * ( preferably in main(), for example, etch_watch_id = 127; ) and rerun.
     * we can set a breakpoint below, or compile in IS_USING_DEBUGBREAK, to break
     * into the debugger at the desired memory allocation. we can then examine the
     * call stack to determine the source of that particular and presumably leaked
     * memory allocation.
     */
    #if IS_USING_ALLOC_ID
    rec->id = next_alloc_id();
    #if IS_TRACKING_ETCHHEAP
    if (etch_watch_id == rec->id)          /* * * * *  watch breakpoint  * * * * */
    #if IS_USING_DEBUGBREAK
        __debugbreak();
    #else
        breakpoint++;  /* set breakpoint here when watching an allocation number */
    #endif  /* IS_USING_DEBUGBREAK */
    #endif  /* #if IS_TRACKING_ETCHHEAP */
    #endif  /* #if IS_USING_ALLOC_ID */

    /* the key for our tracking record key/value pair, is the pointer to heap   
     * memory returned by malloc; i.e. we build a map keyed on memory addresses. 
     * the map therefore stores a void**, a pointer to the pointer to the original
     * malloc. debug_malloc() also allocates 4 bytes for the hashkey, which is 
     * eventually freed by debug_free(). 
     */
    pkey = malloc(sizeof(void*));    /* allocate memory for the hashkey */
    memcpy(pkey, &p, sizeof(void*)); /* copy user's mem* into that key */ 

    #if IS_TRACKING_ETCHHEAP
    if (etch_watch_addr == (size_t) pkey)          
    #if IS_USING_DEBUGBREAK
        __debugbreak();
    #else
        breakpoint++;  /* set breakpoint here when watching for a memory address */
    #endif  /* IS_USING_DEBUGBREAK */
    #endif  /* #if IS_TRACKING_ETCHHEAP */
    
    /* insert this allocation record into the tracking map */
    hashresult = memtable->vtab->insert(memtable->realtable, 
         pkey, sizeof(void*), rec, sizeof(memrec), 0, 0); 
    
    if  (hashresult == -1) 
         etchlog(ETCHMEMX, ETCHLOG_ERROR, 
             "error inserting entry to heap tracking store\n");  
    #if(0)
    else etchlog(ETCHMEMX, ETCHLOG_XDEBUG,"memtbl inserted key '%08x' obj %d size %d\n", 
           (unsigned)p, objtype, nbytes);
    /*
    else printf("\nMEMX memtbl inserted key '%08x' obj %d size %d\n", 
           (unsigned)p, objtype, nbytes);
     */
    #endif
      
    etchheap_currbytes += nbytes;
    if (etchheap_currbytes > etchheap_maxbytes) etchheap_maxbytes = etchheap_currbytes;
    if (++etchheap_count > etchheap_hiwater) etchheap_hiwater = etchheap_count;
    return p;
}


/**
 *  debug_realloc()
 *  tracks mallocs by entering the malloc'ed object info, code file, and code line,
 *  into a tracking table. This is a private method -- it should not be invoked
 *  directly, but rather via the etch_malloc macro, when ETCH_DEBUGALLOC is defined.
 */
void* debug_realloc(void* p, size_t nbytes, const short objtype, 
    char* file, const short line)
{
    void   *new_ptr = NULL;
    memrec *oldrec_ptr = NULL;

    if (p == NULL) /* acts as malloc */
        return debug_malloc(nbytes, objtype, file, line);

    if (nbytes == 0) /* acts as free */
    {
        debug_free(p, file, line);
        return NULL;
    }

    oldrec_ptr = debug_find_memrec(p, file, line);
    ETCH_ASSERT(oldrec_ptr != NULL);
    if (oldrec_ptr == NULL) return NULL;

    new_ptr = debug_malloc(nbytes, objtype, file, line);
    if (new_ptr == NULL) return NULL; /* per realloc spec */

    memcpy(new_ptr, p, min(nbytes, oldrec_ptr->size));

    debug_free(p, file, line);
   
    return new_ptr;
}


/**
 *  debug_free()
 *  tracks free()s by looking up the associated malloc() record and removing it
 *  from the tracking table. this is a private method -- it should not be invoked
 *  directly, but rather via the etch_free macro, when ETCH_DEBUGALLOC is defined.
 */
int debug_free(void* p, char* file, const short line) /* __FILE__ not unicode */
{  
    int  hashresult = 0, buckresult = 0, breakpoint = 0;
    etch_hashitem  hashbucket;
    etch_hashitem* thisentry = &hashbucket;
    void*   this_key = 0;
    memrec* mrec = 0;
    if (!p) return -1;

    do 
    {   if (!memtable) break;

        #if IS_TRACKING_ETCHHEAP
        if (etch_watch_addr == (size_t) p)          
        #if IS_USING_DEBUGBREAK
            __debugbreak();
        #else
            breakpoint++;  /* set breakpoint here when watching for a memory address */
        #endif  /* IS_USING_DEBUGBREAK */
        #endif  /* #if IS_TRACKING_ETCHHEAP */
    
        /* first free the memory tracking table entry. recall that the key to the   
         * tracking record is the value of the heap address returned by malloc. 
         */
        hashresult = memtable->vtab->find   /* retrieve the map entry */
           (memtable->realtable, &p, sizeof(void*), NULL, &thisentry); 

        if  (hashresult < 0)    
        {    etchlog(ETCHMEMX, ETCHLOG_ERROR,            
                "etch heap tracking store missing key '%08x'\n", (unsigned)p);
             break;
        }

        this_key = thisentry->key;
        mrec = (memrec*) thisentry->value;

        #if IS_TRACKING_ETCHHEAP
        if (etch_watch_id == mrec->id)
            breakpoint++;  /* set breakpoint here when watching an alloc number */
        #endif /* #if IS_TRACKING_ETCHHEAP */       
         
        #if(0)
        printf("\nDFRE memtbl freeing key '%08x' obj %d size %d line %d file %s\n",   
              (unsigned)p, mrec->objtype, mrec->size, mrec->line, mrec->file); 
        #endif
         
        buckresult = memtable->vtab->remove    /* free the map bucket */
           (memtable->realtable, &p, sizeof(void*), NULL, &thisentry); 

        etchheap_count--;
        etchheap_currbytes -= mrec->size;
        free(mrec);   /* free memory for the tracking record */
        free(this_key);      /* free memory for the hashkey */

    } while(0);

    free(p);  /* finally free the actual object */
    return hashresult;
}


/** 
 * debug_find_memrec()
 * looks up the memory record in the map for specified address p. 
 * returns NULL if not found.
 */
memrec* debug_find_memrec(void* p, char* file, const short line)  
{  
    etch_hashitem  hashbucket, *thisentry = &hashbucket;
    memrec* this_memrec = NULL;

    if (!p || !memtable);
    else  
    if  (0 == memtable->vtab->find(memtable->realtable, &p, 
              sizeof(void*), NULL, &thisentry)) 
 
         this_memrec = (memrec*) thisentry->value;

    else etchlog(ETCHMEMX, ETCHLOG_ERROR, 
         "etch heap tracking store missing key '%08x'\n", (unsigned)p);  

    return this_memrec;
}


#endif /* #ifdef ETCH_DEBUGALLOC */