/**
BlkInfo thread-local cache. Used for array appending in the conservative GC to avoid the lock when possible.

Note: this used to be in rt.lifetime, but was moved here to allow GCs to take over array operations.
*/
module core.internal.gc.blkcache;

import core.memory;
import core.attribute;

debug (PRINTF) import core.stdc.stdio : printf;

alias BlkInfo = GC.BlkInfo;
alias BlkAttr = GC.BlkAttr;

/**
  cache for the lookup of the block info
  */
private enum N_CACHE_BLOCKS = 8;

// note this is TLS, so no need to sync.
BlkInfo *__blkcache_storage;

static if (N_CACHE_BLOCKS == 1)
{
    version=single_cache;
}
else
{
    //version=simple_cache; // uncomment to test simple cache strategy
    //version=random_cache; // uncomment to test random cache strategy

    // ensure N_CACHE_BLOCKS is power of 2.
    static assert(!((N_CACHE_BLOCKS - 1) & N_CACHE_BLOCKS));

    version (random_cache)
    {
        int __nextRndNum = 0;
    }
    int __nextBlkIdx;
}

@property BlkInfo *__blkcache() nothrow @nogc
{
    if (!__blkcache_storage)
    {
        import core.stdc.stdlib : calloc;
        import core.thread.threadbase;
        auto tBase = ThreadBase.getThis();
        if (tBase is null)
            // if we don't have a thread object, this is a detached thread, and
            // this won't be properly maintained by the GC.
            return null;

        // allocate the block cache for the first time
        immutable size = BlkInfo.sizeof * N_CACHE_BLOCKS;
        // use C alloc, because this may become a detached thread, and the GC
        // would then clean up the cache without zeroing this pointer.
        __blkcache_storage = cast(BlkInfo*) calloc(size, 1);
        tBase.tlsGCData = __blkcache_storage;
    }
    return __blkcache_storage;
}

// free the allocation on thread exit.
void cleanupBlkCache(void* storage) nothrow @nogc
{
    if (storage)
    {
        // check if this is the same thread as the current running thread, and
        // if so, make sure we don't leave a dangling pointer.
        if (__blkcache_storage is storage)
            __blkcache_storage = null;
        import core.stdc.stdlib : free;
        free(storage);
    }
}

/**
 * Indicates whether an address has been marked by the GC.
 */
enum IsMarked : int
{
         no, /// Address is not marked.
        yes, /// Address is marked.
    unknown, /// Address is not managed by the GC.
}

alias IsMarkedDg = IsMarked delegate(void* addr) nothrow; /// The isMarked callback function.

// we expect this to be called with the lock in place
void processGCMarks(void* data, scope IsMarkedDg isMarked) nothrow
{
    if (!data)
        return;

    auto cache = cast(BlkInfo*) data;
    // called after the mark routine to eliminate block cache data when it
    // might be ready to sweep

    debug(PRINTF) printf("processing GC Marks, %p\n", cache);
    debug(PRINTF) foreach (i; 0 .. N_CACHE_BLOCKS)
    {
        printf("cache entry %d has base ptr %p\tsize %zd\tflags %x\n", i, cache[i].base, cache[i].size, cache[i].attr);
    }
    auto cache_end = cache + N_CACHE_BLOCKS;
    for (;cache < cache_end; ++cache)
    {
        if (cache.base != null && isMarked(cache.base) == IsMarked.no)
        {
            debug(PRINTF) printf("clearing cache entry at %p\n", cache.base);
            cache.base = null; // clear that data.
        }
    }
}

unittest
{
    // Bugzilla 10701 - segfault in GC
    ubyte[] result; result.length = 4096;
    GC.free(result.ptr);
    GC.collect();
}

/**
  Get the cached block info of an interior pointer.  Returns null if the
  interior pointer's block is not cached.

  NOTE: The following note was not valid, but is retained for historical
        purposes. The data cannot be cleared because the stack contains a
        reference to the affected block (e.g. through `interior`). Therefore,
        the element will not be collected, and the data will remain valid.

  ORIGINAL: The base ptr in this struct can be cleared asynchronously by the GC,
        so any use of the returned BlkInfo should copy it and then check the
        base ptr of the copy before actually using it.
  */
BlkInfo *__getBlkInfo(void *interior) nothrow @nogc
{
    BlkInfo *ptr = __blkcache;
    if (ptr is null)
        // if for some reason we don't have a cache, return null.
        return null;
    version (single_cache)
    {
        if (ptr.base && ptr.base <= interior && (interior - ptr.base) < ptr.size)
            return ptr;
        return null; // not in cache.
    }
    else version (simple_cache)
    {
        foreach (i; 0..N_CACHE_BLOCKS)
        {
            if (ptr.base && ptr.base <= interior && (interior - ptr.base) < ptr.size)
                return ptr;
            ptr++;
        }
    }
    else
    {
        // try to do a smart lookup, using __nextBlkIdx as the "head"
        auto curi = ptr + __nextBlkIdx;
        for (auto i = curi; i >= ptr; --i)
        {
            if (i.base && i.base <= interior && cast(size_t)(interior - i.base) < i.size)
                return i;
        }

        for (auto i = ptr + N_CACHE_BLOCKS - 1; i > curi; --i)
        {
            if (i.base && i.base <= interior && cast(size_t)(interior - i.base) < i.size)
                return i;
        }
    }
    return null; // not in cache.
}

void __insertBlkInfoCache(BlkInfo bi, BlkInfo *curpos) nothrow @nogc
{
    auto cache = __blkcache;
    if (cache is null)
        // no cache to use.
        return;

    version (single_cache)
    {
        *cache = bi;
        return;
    }
    else
    {
        version (simple_cache)
        {
            if (curpos)
                *curpos = bi;
            else
            {
                // note, this is a super-simple algorithm that does not care about
                // most recently used.  It simply uses a round-robin technique to
                // cache block info.  This means that the ordering of the cache
                // doesn't mean anything.  Certain patterns of allocation may
                // render the cache near-useless.
                cache[__nextBlkIdx] = bi;
                __nextBlkIdx = (__nextBlkIdx+1) & (N_CACHE_BLOCKS - 1);
            }
        }
        else version (random_cache)
        {
            // strategy: if the block currently is in the cache, move the
            // current block index to the a random element and evict that
            // element.
            if (!curpos)
            {
                __nextBlkIdx = (__nextRndNum = 1664525 * __nextRndNum + 1013904223) & (N_CACHE_BLOCKS - 1);
                curpos = cache + __nextBlkIdx;
            }
            else
            {
                __nextBlkIdx = curpos - cache;
            }
            *curpos = bi;
        }
        else
        {
            //
            // strategy: If the block currently is in the cache, swap it with
            // the head element.  Otherwise, move the head element up by one,
            // and insert it there.
            //
            if (!curpos)
            {
                __nextBlkIdx = (__nextBlkIdx+1) & (N_CACHE_BLOCKS - 1);
                curpos = cache + __nextBlkIdx;
            }
            else if (curpos !is cache + __nextBlkIdx)
            {
                *curpos = cache[__nextBlkIdx];
                curpos = cache + __nextBlkIdx;
            }
            *curpos = bi;
        }
    }
}

debug(PRINTF)
{
    extern(C) void printArrayCache()
    {
        auto ptr = __blkcache;
        printf("CACHE: \n");
        foreach (i; 0 .. N_CACHE_BLOCKS)
        {
            printf("  %d\taddr:% .8p\tsize:% .10zd\tflags:% .8x\n", i, ptr[i].base, ptr[i].size, ptr[i].attr);
        }
    }
}
