/**
 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;
    void[] gc_getArrayUsed(void *ptr, bool atomic) nothrow pure;
}

/**
Shrink the "allocated" length of an array to be the exact size of the array.

It doesn't matter what the current allocated length of the array is, the
user is telling the runtime that he knows what he is doing.

Params:
    T = the type of the elements in the array (this should be unqualified)
    arr = array to shrink. Its `.length` is element length, not byte length, despite `void` type
    isshared = true if the underlying data is shared
*/
void _d_arrayshrinkfit(Tarr: T[], T)(Tarr arr, bool isshared) @trusted
{
    import core.exception : onFinalizeError;

    debug(PRINTF) printf("_d_arrayshrinkfit, elemsize = %zd, arr.ptr = %p arr.length = %zd\n", T.sizeof, arr.ptr, arr.length);
    auto reqlen = arr.length;

    auto curArr = cast(Tarr)gc_getArrayUsed(arr.ptr, isshared);
    if (curArr.ptr is null)
        // not a valid GC pointer
        return;

    // align the array.
    auto offset = arr.ptr - curArr.ptr;
    auto curlen = curArr.length - offset;
    if (curlen <= reqlen)
        // invalid situation, or no change.
        return;

    // if the type has a destructor, destroy elements we are about to remove.
    static if(is(T == struct) && __traits(needsDestruction, T))
    {
        try
        {
            // Finalize the elements that are being removed

            // Due to the fact that the delete operator calls destructors
            // for arrays from the last element to the first, we maintain
            // compatibility here by doing the same.
            for (auto curP = arr.ptr + curlen - 1; curP >= arr.ptr + reqlen; curP--)
            {
                // call destructor
                curP.__xdtor();
            }
        }
        catch (Exception e)
        {
            onFinalizeError(typeid(T), e);
        }
    }

    gc_shrinkArrayUsed(arr[0 .. reqlen], curlen * T.sizeof, isshared);
}

size_t _d_arraygetcapacity(size_t size, void[]* p, bool isshared) pure nothrow @trusted
in
{
    assert(!(*p).length || (*p).ptr);
}
do
{
    auto datasize = (*p).length * size;
    auto curCapacity = gc_reserveArrayCapacity((*p).ptr[0 .. datasize], 0, isshared);
    return curCapacity / size;
}

/**
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.checkedint : mulu;
    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;

    bool overflow = false;
    const reqsize = mulu(size, newcapacity, overflow);
    if (overflow)
    {
        onOutOfMemoryError();
        assert(0);
    }

    // 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.
    version (D_TypeInfo)
        static enum ti = typeid(T);
    else
        static enum ti = null;
    auto ptr = GC.malloc(reqsize, attrs, ti);
    if (ptr is null)
    {
        onOutOfMemoryError();
        assert(0);
    }

    // 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);

    static if (isShared)
    {
        // This low-level primitive mutates the caller's shared slice header, so
        // the caller must already provide whatever synchronization makes that
        // header update valid; the cast only preserves that existing contract
        // under `-preview=nosharedaccess`.
        *cast(UnqT[]*) &arr = unqual_arr;
    }
    else
        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, "__xpostblit");
    enum hasEnabledPostblit = hasPostblit && !__traits(isDisabled, T.__xpostblit);

    bool overflow = false;
    const newsize = mulu(sizeelem, newlength, overflow);
    if (overflow)
    {
        onOutOfMemoryError();
        assert(0);
    }

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

    enum uint gcAttrs = BlkAttr.APPENDABLE | GC.convertAlignmentToBlkAttr(T.alignof)
        | (is(T == struct) && __traits(hasMember, T, "xdtor") ? BlkAttr.FINALIZE : 0);

    if (!arr.ptr)
    {
        assert(arr.length == 0);
        version (D_TypeInfo)
            void* ptr = GC.malloc(newsize, gcAttrs, typeid(T));
        else
            void* ptr = GC.malloc(newsize, gcAttrs, null);
        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))
    {
        version (D_TypeInfo)
            newdata = GC.malloc(newsize, gcAttrs, typeid(T));
        else
            newdata = GC.malloc(newsize, gcAttrs, null);
        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;
}


// @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);
    // The resized slice has not been published yet, so the test may inspect
    // the backing storage directly to verify initialization.
    foreach (s; (() @trusted => *cast(S[]*) &arr2)())
        assert(s == S.init);
}
