/**
 This module contains utility functions to help the implementation of the runtime hook

  Copyright: Copyright Digital Mars 2000 - 2019.
  License: Distributed under the
       $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost Software License 1.0).
     (See accompanying file LICENSE)
  Source: $(DRUNTIMESRC core/internal/_array/_utils.d)
*/
module core.internal.array.utils;

import core.internal.traits : Parameters;
import core.memory : GC;

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

private
{
    enum : size_t
    {
        PAGESIZE = 4096,
        BIGLENGTHMASK = ~(PAGESIZE - 1),
        SMALLPAD = 1,
        MEDPAD = ushort.sizeof,
        LARGEPREFIX = 16, // 16 bytes padding at the front of the array
        LARGEPAD = LARGEPREFIX + 1,
        MAXSMALLSIZE = 256-SMALLPAD,
        MAXMEDSIZE = (PAGESIZE / 2) - MEDPAD
    }
}

auto gcStatsPure() nothrow pure
{
    import core.memory : GC;

    auto impureBypass = cast(GC.Stats function() pure nothrow)&GC.stats;
    return impureBypass();
}

ulong accumulatePure(string file, int line, string funcname, string name, ulong size) nothrow pure
{
    static ulong impureBypass(string file, int line, string funcname, string name, ulong size) @nogc nothrow
    {
        import core.internal.traits : externDFunc;

        alias accumulate = externDFunc!("rt.profilegc.accumulate", void function(string file, uint line, string funcname, string type, ulong sz) @nogc nothrow);
        accumulate(file, line, funcname, name, size);
        return size;
    }

    auto func = cast(ulong function(string file, int line, string funcname, string name, ulong size) @nogc nothrow pure)&impureBypass;
    return func(file, line, funcname, name, size);
}

version (D_ProfileGC)
{
    /**
     * TraceGC wrapper generator around the runtime hook `Hook`.
     * Params:
     *   Type = The type of hook to report to accumulate
     *   Hook = The name hook to wrap
     */
    template TraceHook(string Type, string Hook)
    {
        const char[] TraceHook = q{
            import core.internal.array.utils : gcStatsPure, accumulatePure;

            pragma(inline, false);
            string name = } ~ "`" ~ Type ~ "`;" ~ q{

            // FIXME: use rt.tracegc.accumulator when it is accessable in the future.
            version (tracegc)
        } ~ "{\n" ~ q{
                import core.stdc.stdio;

                printf("%sTrace file = '%.*s' line = %d function = '%.*s' type = %.*s\n",
                } ~ "\"" ~ Hook ~ "\".ptr," ~ q{
                    file.length, file.ptr,
                    line,
                    funcname.length, funcname.ptr,
                    name.length, name.ptr
                );
            } ~ "}\n" ~ q{
            ulong currentlyAllocated = gcStatsPure().allocatedInCurrentThread;

            scope(exit)
            {
                ulong size = gcStatsPure().allocatedInCurrentThread - currentlyAllocated;
                if (size > 0)
                    if (!accumulatePure(file, line, funcname, name, size)) {
                        // This 'if' and 'assert' is needed to force the compiler to not remove the call to
                        // `accumulatePure`. It really want to do that while optimizing as the function is
                        // `pure` and it does not influence the result of this hook.

                        // `accumulatePure` returns the value of `size`, which can never be zero due to the
                        // previous 'if'. So this assert will never be triggered.
                        assert(0);
                    }
            }
        };
    }

    /**
     * TraceGC wrapper around runtime hook `Hook`.
     * Params:
     *  T = Type of hook to report to accumulate
     *  Hook = The hook to wrap
     *  errorMessage = The error message incase `version != D_TypeInfo`
     *  file = File that called `_d_HookTraceImpl`
     *  line = Line inside of `file` that called `_d_HookTraceImpl`
     *  funcname = Function that called `_d_HookTraceImpl`
     *  parameters = Parameters that will be used to call `Hook`
     * Bugs:
     *  This function template needs be between the compiler and a much older runtime hook that bypassed safety,
     *  purity, and throwabilty checks. To prevent breaking existing code, this function template
     *  is temporarily declared `@trusted pure` until the implementation can be brought up to modern D expectations.
    */
    auto _d_HookTraceImpl(T, alias Hook, string errorMessage)(string file, int line, string funcname, Parameters!Hook parameters) @trusted pure
    {
        version (D_TypeInfo)
        {
            mixin(TraceHook!(T.stringof, __traits(identifier, Hook)));
            return Hook(parameters);
        }
        else
            assert(0, errorMessage);
    }
}

/**
 * Check if the function `F` is calleable in a `nothrow` scope.
 * Params:
 *  F = Function that does not take any parameters
 * Returns:
 *  if the function is callable in a `nothrow` scope.
 */
enum isNoThrow(alias F) = is(typeof(() nothrow { F(); }));

/**
 * Check if the type `T`'s postblit is called in nothrow, if it exist
 * Params:
 *  T = Type to check
 * Returns:
 *  if the postblit is callable in a `nothrow` scope, if it exist.
 *  if it does not exist, return true.
 */
template isPostblitNoThrow(T) {
    static if (__traits(isStaticArray, T))
        enum isPostblitNoThrow = isPostblitNoThrow!(typeof(T.init[0]));
    else static if (__traits(hasMember, T, "__xpostblit") &&
        // Bugzilla 14746: Check that it's the exact member of S.
        __traits(isSame, T, __traits(parent, T.init.__xpostblit)))
        enum isPostblitNoThrow = isNoThrow!(T.init.__xpostblit);
    else
        enum isPostblitNoThrow = true;
}

/**
 * Clear padding that might not be zeroed by the GC (it assumes it is within the
 * requested size from the start, but it is actually at the end of the allocated
 * block).
 *
 * Params:
 *  info = array allocation data
 *  arrSize = size of the array in bytes
 *  padSize = size of the padding in bytes
 */
void __arrayClearPad()(ref BlkInfo info, size_t arrSize, size_t padSize) nothrow pure
{
    import core.stdc.string;
    if (padSize > MEDPAD && !(info.attr & BlkAttr.NO_SCAN) && info.base)
    {
        if (info.size < PAGESIZE)
            memset(info.base + arrSize, 0, padSize);
        else
            memset(info.base, 0, LARGEPREFIX);
    }
}

/**
 * Allocate an array memory block by applying the proper padding and assigning
 * block attributes if not inherited from the existing block.
 *
 * Params:
 *  arrSize = size of the allocated array in bytes
 * Returns:
 *  `BlkInfo` with allocation metadata
 */
BlkInfo __arrayAlloc(T)(size_t arrSize) @trusted
{
    import core.checkedint;
    import core.lifetime : TypeInfoSize;
    import core.internal.traits : hasIndirections;

    enum typeInfoSize = TypeInfoSize!T;
    BlkAttr attr = BlkAttr.APPENDABLE;

    size_t padSize = arrSize > MAXMEDSIZE ?
        LARGEPAD :
        ((arrSize > MAXSMALLSIZE ? MEDPAD : SMALLPAD) + typeInfoSize);

    bool overflow;
    auto paddedSize = addu(arrSize, padSize, overflow);

    if (overflow)
        return BlkInfo();

    /* `extern(C++)` classes don't have a classinfo pointer in their vtable,
     * so the GC can't finalize them.
     */
    static if (typeInfoSize)
        attr |= BlkAttr.STRUCTFINAL | BlkAttr.FINALIZE;
    static if (!hasIndirections!T)
        attr |= BlkAttr.NO_SCAN;

    auto bi = GC.qalloc(paddedSize, attr, typeid(T));
    __arrayClearPad(bi, arrSize, padSize);
    return bi;
}

/**
 * Get the start of the array for the given block.
 *
 * Params:
 *  info = array metadata
 * Returns:
 *  pointer to the start of the array
 */
void *__arrayStart()(return scope BlkInfo info) nothrow pure
{
    return info.base + ((info.size & BIGLENGTHMASK) ? LARGEPREFIX : 0);
}

/**
 * Set the allocated length of the array block.  This is called when an array
 * is appended to or its length is set.
 *
 * The allocated block looks like this for blocks < PAGESIZE:
 * `|elem0|elem1|elem2|...|elemN-1|emptyspace|N*elemsize|`
 *
 * The size of the allocated length at the end depends on the block size:
 *      a block of 16 to 256 bytes has an 8-bit length.
 *      a block with 512 to pagesize/2 bytes has a 16-bit length.
 *
 * For blocks >= pagesize, the length is a size_t and is at the beginning of the
 * block.  The reason we have to do this is because the block can extend into
 * more pages, so we cannot trust the block length if it sits at the end of the
 * block, because it might have just been extended.  If we can prove in the
 * future that the block is unshared, we may be able to change this, but I'm not
 * sure it's important.
 *
 * In order to do put the length at the front, we have to provide 16 bytes
 * buffer space in case the block has to be aligned properly.  In x86, certain
 * SSE instructions will only work if the data is 16-byte aligned.  In addition,
 * we need the sentinel byte to prevent accidental pointers to the next block.
 * Because of the extra overhead, we only do this for page size and above, where
 * the overhead is minimal compared to the block size.
 *
 * So for those blocks, it looks like:
 * `|N*elemsize|padding|elem0|elem1|...|elemN-1|emptyspace|sentinelbyte|``
 *
 * where `elem0` starts 16 bytes after the first byte.
 */
bool __setArrayAllocLength(T)(ref BlkInfo info, size_t newLength, bool isShared, size_t oldLength = ~0)
{
    import core.atomic;
    import core.lifetime : TypeInfoSize;

    size_t typeInfoSize = TypeInfoSize!T;

    if (info.size <= 256)
    {
        import core.checkedint;

        bool overflow;
        auto newLengthPadded = addu(newLength,
                                     addu(SMALLPAD, typeInfoSize, overflow),
                                     overflow);

        if (newLengthPadded > info.size || overflow)
            // new size does not fit inside block
            return false;

        auto length = cast(ubyte *)(info.base + info.size - typeInfoSize - SMALLPAD);
        if (oldLength != ~0)
        {
            if (isShared)
            {
                return cas(cast(shared)length, cast(ubyte)oldLength, cast(ubyte)newLength);
            }
            else
            {
                if (*length == cast(ubyte)oldLength)
                    *length = cast(ubyte)newLength;
                else
                    return false;
            }
        }
        else
        {
            // setting the initial length, no cas needed
            *length = cast(ubyte)newLength;
        }
        if (typeInfoSize)
        {
            auto typeInfo = cast(TypeInfo*)(info.base + info.size - size_t.sizeof);
            *typeInfo = cast()typeid(T);
        }
    }
    else if (info.size < PAGESIZE)
    {
        if (newLength + MEDPAD + typeInfoSize > info.size)
            // new size does not fit inside block
            return false;
        auto length = cast(ushort *)(info.base + info.size - typeInfoSize - MEDPAD);
        if (oldLength != ~0)
        {
            if (isShared)
            {
                return cas(cast(shared)length, cast(ushort)oldLength, cast(ushort)newLength);
            }
            else
            {
                if (*length == oldLength)
                    *length = cast(ushort)newLength;
                else
                    return false;
            }
        }
        else
        {
            // setting the initial length, no cas needed
            *length = cast(ushort)newLength;
        }
        if (typeInfoSize)
        {
            auto typeInfo = cast(TypeInfo*)(info.base + info.size - size_t.sizeof);
            *typeInfo = cast()typeid(T);
        }
    }
    else
    {
        if (newLength + LARGEPAD > info.size)
            // new size does not fit inside block
            return false;
        auto length = cast(size_t *)(info.base);
        if (oldLength != ~0)
        {
            if (isShared)
            {
                return cas(cast(shared)length, cast(size_t)oldLength, cast(size_t)newLength);
            }
            else
            {
                if (*length == oldLength)
                    *length = newLength;
                else
                    return false;
            }
        }
        else
        {
            // setting the initial length, no cas needed
            *length = newLength;
        }
        if (typeInfoSize)
        {
            auto typeInfo = cast(TypeInfo*)(info.base + size_t.sizeof);
            *typeInfo = cast()typeid(T);
        }
    }
    return true; // resize succeeded
}
