/**
 * Varargs implementation for the AArch64 Procedure Call Standard (not followed by Apple).
 * Used by core.stdc.stdarg and core.vararg.
 *
 * Reference: https://github.com/ARM-software/abi-aa/blob/master/aapcs64/aapcs64.rst#appendix-variable-argument-lists
 *
 * Copyright: Copyright Digital Mars 2020 - 2020.
 * License:   $(HTTP www.boost.org/LICENSE_1_0.txt, Boost License 1.0).
 * Authors:   Martin Kinkelin
 * Source: $(DRUNTIMESRC core/internal/vararg/aarch64.d)
 */

module core.internal.vararg.aarch64;

version (AArch64):

// Darwin uses a simpler varargs implementation
version (OSX) {}
else version (iOS) {}
else version (TVOS) {}
else version (WatchOS) {}
else:

import core.stdc.stdarg : alignUp;

@system:
//@nogc:    // Not yet, need to make TypeInfo's member functions @nogc first
nothrow:

extern (C++, std) struct __va_list
{
    void* __stack;
    void* __gr_top;
    void* __vr_top;
    int __gr_offs;
    int __vr_offs;
}

///
alias va_list = __va_list;

///
T va_arg(T)(ref va_list ap)
{
    static if (is(T ArgTypes == __argTypes))
    {
        T onStack()
        {
            void* arg = ap.__stack;
            static if (T.alignof > 8)
                arg = arg.alignUp!16;
            ap.__stack = alignUp(arg + T.sizeof);
            version (BigEndian)
                static if (T.sizeof < 8)
                    arg += 8 - T.sizeof;
            return *cast(T*) arg;
        }

        static if (ArgTypes.length == 0)
        {
            // indirectly by value; get pointer and copy
            T* ptr = va_arg!(T*)(ap);
            return *ptr;
        }

        static assert(ArgTypes.length == 1);

        static if (is(ArgTypes[0] E : E[N], int N))
            alias FundamentalType = E; // static array element type
        else
            alias FundamentalType = ArgTypes[0];

        static if (__traits(isFloating, FundamentalType) || is(FundamentalType == __vector))
        {
            import core.stdc.string : memcpy;

            // SIMD register(s)
            int offs = ap.__vr_offs;
            if (offs >= 0)
                return onStack();           // reg save area empty
            enum int usedRegSize = FundamentalType.sizeof;
            static assert(T.sizeof % usedRegSize == 0);
            enum int nreg = T.sizeof / usedRegSize;
            ap.__vr_offs = offs + (nreg * 16);
            if (ap.__vr_offs > 0)
                return onStack();           // overflowed reg save area
            version (BigEndian)
                static if (usedRegSize < 16)
                    offs += 16 - usedRegSize;

            T result = void;
            static foreach (i; 0 .. nreg)
                memcpy((cast(void*) &result) + i * usedRegSize, ap.__vr_top + (offs + i * 16), usedRegSize);
            return result;
        }
        else
        {
            // GP register(s)
            int offs = ap.__gr_offs;
            if (offs >= 0)
                return onStack();           // reg save area empty
            static if (T.alignof > 8)
                offs = offs.alignUp!16;
            enum int nreg = (T.sizeof + 7) / 8;
            ap.__gr_offs = offs + (nreg * 8);
            if (ap.__gr_offs > 0)
                return onStack();           // overflowed reg save area
            version (BigEndian)
                static if (T.sizeof < 8)
                    offs += 8 - T.sizeof;
            return *cast(T*) (ap.__gr_top + offs);
        }
    }
    else
    {
        static assert(false, "not a valid argument type for va_arg");
    }
}

///
void va_arg()(ref va_list ap, TypeInfo ti, void* parmn)
{
    import core.stdc.string : memcpy;

    const size = ti.tsize;
    const alignment = ti.talign;

    if (auto ti_struct = cast(TypeInfo_Struct) ti)
    {
        TypeInfo arg1, arg2;
        ti.argTypes(arg1, arg2);

        if (!arg1)
        {
            // indirectly by value; get pointer and move
            void* ptr = va_arg!(void*)(ap);
            memcpy(parmn, ptr, size);
            return;
        }

        assert(!arg2);
        ti = arg1;
    }

    void onStack()
    {
        void* arg = ap.__stack;
        if (alignment > 8)
            arg = arg.alignUp!16;
        ap.__stack = alignUp(arg + size);
        version (BigEndian)
            if (size < 8)
                arg += 8 - size;
        memcpy(parmn, arg, size);
    }

    // HFVA structs have already been lowered to static arrays;
    // lower `ti` further to the fundamental type, including HFVA
    // static arrays.
    // TODO: complex numbers
    if (auto ti_sarray = cast(TypeInfo_StaticArray) ti)
        ti = ti_sarray.value;

    if (ti.flags() & 2)
    {
        // SIMD register(s)
        int offs = ap.__vr_offs;
        if (offs >= 0)
            return onStack();           // reg save area empty
        const usedRegSize = cast(int) ti.tsize;
        assert(size % usedRegSize == 0);
        const nreg = cast(int) (size / usedRegSize);
        ap.__vr_offs = offs + (nreg * 16);
        if (ap.__vr_offs > 0)
            return onStack();           // overflowed reg save area
        version (BigEndian)
            if (usedRegSize < 16)
                offs += 16 - usedRegSize;
        foreach (i; 0 .. nreg)
            memcpy(parmn + i * usedRegSize, ap.__vr_top + (offs + i * 16), usedRegSize);

        return;
    }

    // GP register(s)
    int offs = ap.__gr_offs;
    if (offs >= 0)
        return onStack();           // reg save area empty
    if (alignment > 8)
        offs = offs.alignUp!16;
    const nreg = cast(int) ((size + 7) / 8);
    ap.__gr_offs = offs + (nreg * 8);
    if (ap.__gr_offs > 0)
        return onStack();           // overflowed reg save area
    version (BigEndian)
        if (size < 8)
            offs += 8 - size;
    memcpy(parmn, ap.__gr_top + offs, size);
}
