/**
 This module contains support for controlling dynamic arrays' capacity and length

  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/_capacity.d)
*/
module core.internal.array.capacity;

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

// for now, all GC array functions are not exposed via core.memory.
extern(C) {
    bool gc_expandArrayUsed(void[] slice, size_t newUsed, bool atomic) nothrow pure;
    size_t gc_reserveArrayCapacity(void[] slice, size_t request, bool atomic) nothrow pure;
    bool gc_shrinkArrayUsed(void[] slice, size_t existingUsed, bool atomic) nothrow pure;
}
/**
Set the array capacity.

If the array capacity isn't currently large enough
to hold the requested capacity (in number of elements), then the array is
resized/reallocated to the appropriate size.

Pass in a requested capacity of 0 to get the current capacity.

Params:
    T = the type of the elements in the array (this should be unqualified)
    newcapacity = requested new capacity
    p = pointer to array to set. Its `length` is left unchanged.
    isshared = true if the underlying data is shared

Returns: the number of elements that can actually be stored once the resizing is done
*/
size_t _d_arraysetcapacityPureNothrow(T)(size_t newcapacity, void[]* p, bool isshared) pure nothrow @trusted
do
{
    alias PureNothrowType = size_t function(size_t, void[]*, bool) pure nothrow @trusted;
    return (cast(PureNothrowType) &_d_arraysetcapacity!T)(newcapacity, p, isshared);
}

size_t _d_arraysetcapacity(T)(size_t newcapacity, void[]* p, bool isshared) @trusted
in
{
    assert(!(*p).length || (*p).ptr);
}
do
{
    import core.exception : onOutOfMemoryError;
    import core.stdc.string : memcpy, memset;
    import core.internal.array.utils: __typeAttrs;
    import core.internal.lifetime : __doPostblit;

    import core.memory : GC;

    alias BlkAttr = GC.BlkAttr;

    auto size = T.sizeof;
    version (D_InlineAsm_X86)
    {
        size_t reqsize = void;

        asm nothrow pure
        {
            mov EAX, newcapacity;
            mul EAX, size;
            mov reqsize, EAX;
            jnc Lcontinue;
        }
    }
    else version (D_InlineAsm_X86_64)
    {
        size_t reqsize = void;

        asm nothrow pure
        {
            mov RAX, newcapacity;
            mul RAX, size;
            mov reqsize, RAX;
            jnc Lcontinue;
        }
    }
    else
    {
        bool overflow = false;
        size_t reqsize = mulu(size, newcapacity, overflow);
        if (!overflow)
            goto Lcontinue;
    }
Loverflow:
    onOutOfMemoryError();
    assert(0);
Lcontinue:

    // step 1, see if we can ensure the capacity is valid in-place
    auto datasize = (*p).length * size;
    auto curCapacity = gc_reserveArrayCapacity((*p).ptr[0 .. datasize], reqsize, isshared);
    if (curCapacity != 0) // in-place worked!
        return curCapacity / size;

    if (reqsize <= datasize) // requested size is less than array size, the current array satisfies
        // the request. But this is not an appendable GC array, so return 0.
        return 0;

    // step 2, if reserving in-place doesn't work, allocate a new array with at
    // least the requested allocated size.
    auto attrs = __typeAttrs!T((*p).ptr) | BlkAttr.APPENDABLE;

    // use this static enum to avoid recomputing TypeInfo for every call.
    static enum ti = typeid(T);
    auto ptr = GC.malloc(reqsize, attrs, ti);
    if (ptr is null)
        goto Loverflow;

    // copy the data over.
    // note that malloc will have initialized the data we did not request to 0.
    memcpy(ptr, (*p).ptr, datasize);

    // handle postblit
    __doPostblit!T(cast(T[])ptr[0 .. datasize]);

    if (!(attrs & BlkAttr.NO_SCAN))
    {
        // need to memset the newly requested data, except for the data that
        // malloc returned that we didn't request.
        void* endptr = ptr + reqsize;
        void* begptr = ptr + datasize;

        // sanity check
        assert(endptr >= begptr);
        memset(begptr, 0, endptr - begptr);
    }

    *p = ptr[0 .. (*p).length];

    // set up the correct length. Note that we need to do this here, because
    // the GC malloc will automatically set the used size to what we requested.
    gc_shrinkArrayUsed(ptr[0 .. datasize], reqsize, isshared);

    curCapacity = gc_reserveArrayCapacity(ptr[0 .. datasize], 0, isshared);
    assert(curCapacity);
    return curCapacity / size;
}

/**
Resize a dynamic array by setting its `.length` property.

Newly created elements are initialized based on their default value.
If the array's elements initialize to `0`, memory is zeroed out. Otherwise, elements are explicitly initialized.

This function handles memory allocation, expansion, and initialization while maintaining array integrity.

---
void main()
{
    int[] a = [1, 2];
    a.length = 3; // Gets lowered to `_d_arraysetlengthT!(int)(a, 3, false)`
}
---

Params:
    arr         = The array to resize.
    newlength   = The new value for the array's `.length`.

Returns:
    The resized array with updated length and properly initialized elements.

Throws:
    OutOfMemoryError if allocation fails.
*/
size_t _d_arraysetlengthT(Tarr : T[], T)(return ref scope Tarr arr, size_t newlength) @trusted
{
    import core.internal.traits : Unqual;

    // Check if the type is shared
    enum isShared = is(T == shared);

    // Unqualify the type to remove `const`, `immutable`, `shared`, etc.
    alias UnqT = Unqual!T;

    // Cast the array to the unqualified type
    auto unqual_arr = cast(UnqT[]) arr;

    // Call the implementation with the unqualified array and sharedness flag
    size_t result = _d_arraysetlengthT_(unqual_arr, newlength, isShared);

    arr = cast(Tarr) unqual_arr;
    // Return the result
    return result;
}

private size_t _d_arraysetlengthT_(Tarr : T[], T)(return ref scope Tarr arr, size_t newlength, bool isShared) @trusted
{
    import core.checkedint : mulu;
    import core.exception : onFinalizeError, onOutOfMemoryError;
    import core.stdc.string : memcpy, memset;
    import core.internal.traits : hasElaborateCopyConstructor, Unqual;
    import core.lifetime : emplace;
    import core.memory;
    import core.internal.lifetime : __doPostblit;

    alias BlkAttr = GC.BlkAttr;
    alias UnqT = Unqual!T;

    debug(PRINTF)
    {
        printf("_d_arraysetlengthT(arr.ptr = %p, arr.length = %zd, newlength = %zd)\n",
            arr.ptr, arr.length, newlength);
    }

    // If the new length is less than or equal to the current length, just truncate the array
    if (newlength <= arr.length)
    {
        arr = arr[0 .. newlength];
        return newlength;
    }

    enum sizeelem = T.sizeof;
    enum hasPostblit = __traits(hasMember, T, "__postblit");
    enum hasEnabledPostblit = hasPostblit && !__traits(isDisabled, T.__postblit);

    ubyte overflow = 0;

    size_t newsize = void;

    version (D_InlineAsm_X86)
    {
        asm pure nothrow @nogc
        {
            mov EAX, sizeelem;
            mul newlength;        // EDX:EAX = EAX * newlength
            mov newsize, EAX;
            setc overflow;
        }
    }
    else version (D_InlineAsm_X86_64)
    {
        asm pure nothrow @nogc
        {
            mov RAX, sizeelem;
            mul newlength;        // RDX:RAX = RAX * newlength
            mov newsize, RAX;
            setc overflow;
        }
    }
    else
    {
        newsize = mulu(sizeelem, newlength, overflow);
    }

    if (overflow)
    {
        onOutOfMemoryError();
        assert(0);
    }

    debug(PRINTF) printf("newsize = %zx\n", newsize);

    uint gcAttrs = BlkAttr.APPENDABLE;
    static if (is(T == struct) && __traits(hasMember, T, "xdtor"))
    {
        gcAttrs |= BlkAttr.FINALIZE;
    }

    if (!arr.ptr)
    {
        assert(arr.length == 0);
        void* ptr = GC.malloc(newsize, gcAttrs);
        if (!ptr)
        {
            onOutOfMemoryError();
            assert(0);
        }

        static if (__traits(isZeroInit, T))
        {
            memset(ptr, 0, newsize);
        }
        else static if (hasElaborateCopyConstructor!T && !hasPostblit)
        {
            foreach (i; 0 .. newlength)
                emplace(cast(UnqT*) ptr + i, UnqT.init); // safe default construction
        }
        else
        {
            auto temp = UnqT.init;
            foreach (i; 0 .. newlength)
                memcpy(cast(UnqT*) ptr + i, cast(const void*)&temp, T.sizeof);

            static if (hasEnabledPostblit)
                __doPostblit!T((cast(T*) ptr)[0 .. newlength]);
        }

        arr = (cast(T*) ptr)[0 .. newlength];
        return newlength;
    }

    size_t oldsize = arr.length * sizeelem;

    auto newdata = cast(void*) arr.ptr;

    if (!gc_expandArrayUsed(newdata[0 .. oldsize], newsize, isShared))
    {
        newdata = GC.malloc(newsize, gcAttrs);
        if (!newdata)
        {
            onOutOfMemoryError();
            assert(0);
        }
        static if (hasElaborateCopyConstructor!T && !hasPostblit)
        {
            // Use emplace for types with copy constructors but not postblit
            foreach (i; 0 .. arr.length)
                emplace(cast(UnqT*)newdata + i, arr[i]); // safe copy
        }
        else
        {
            memcpy(newdata, cast(const(void)*)arr.ptr, oldsize);

            // Postblit handling for types with postblit, but ensure it compiles
            static if (hasEnabledPostblit)
                __doPostblit!T((cast(T*) (cast(ubyte*)newdata))[0 .. arr.length]);
        }
    }

    // Handle initialization based on whether the type requires zero-init
    static if (__traits(isZeroInit, T))
        memset(cast(void*) (cast(ubyte*)newdata + oldsize), 0, newsize - oldsize);
    else static if (hasElaborateCopyConstructor!T && !hasPostblit)
    {
        foreach (i; 0 .. newlength - arr.length)
            emplace(cast(UnqT*) (cast(ubyte*)newdata + oldsize) + i, UnqT.init);
    }
    else
    {
        auto temp = UnqT.init;
        foreach (i; 0 .. newlength - arr.length)
            memcpy(cast(UnqT*) (cast(ubyte*)newdata + oldsize) + i, cast(const void*)&temp, T.sizeof);

        static if (hasEnabledPostblit)
            __doPostblit!T((cast(T*) (cast(ubyte*)newdata + oldsize))[0 .. newlength - arr.length]);
    }

    arr = (cast(T*) newdata)[0 .. newlength];
    return newlength;
}

version (D_ProfileGC)
{
    enum errorMessage = "Cannot resize arrays";
    import core.internal.array.utils : _d_HookTraceImpl;

    // Function wrapper around the hook, so it’s callable
    size_t _d_arraysetlengthTTrace(Tarr : T[], T)(
        return ref scope Tarr arr,
        size_t newlength,
        string file = __FILE__,
        int line = __LINE__,
        string func = __FUNCTION__
    ) @trusted
    {
        alias Hook = _d_HookTraceImpl!(Tarr, _d_arraysetlengthT!Tarr, errorMessage);
        return Hook(arr, newlength, file, line, func);
    }
}

// @safe unittest remains intact
@safe unittest
{
    struct S
    {
        float f = 1.0;
    }

    int[] arr;
    _d_arraysetlengthT!(typeof(arr))(arr, 16);
    assert(arr.length == 16);
    foreach (int i; arr)
        assert(i == int.init);

    shared S[] arr2;
    _d_arraysetlengthT!(typeof(arr2))(arr2, 16);
    assert(arr2.length == 16);
    foreach (s; arr2)
        assert(s == S.init);
}
