// SPDX-License-Identifier: MIT
// SPDX-FileCopyrightText: 2025 Niels Martignène <niels.martignene@protonmail.com>

#pragma once

#include "lib/native/base/base.hh"
#include "ffi.hh"
#include "util.hh"

#include <napi.h>

namespace K {

bool AnalyseFunction(Napi::Env env, InstanceData *instance, FunctionInfo *func);

struct BackRegisters;

// I'm not sure why the alignas(8), because alignof(CallData) is 8 without it.
// But on Windows i386, without it, the alignment may not be correct (compiler bug?).
struct alignas(8) CallData {
    struct OutArgument {
        enum class Kind {
            Array,
            Buffer,
            String,
            String16,
            String32,
            Object
        };

        Kind kind;

        napi_ref ref;
        const uint8_t *ptr;
        const TypeInfo *type;

        Size max_len; // Only for indirect strings
    };

    Napi::Env env;
    InstanceData *instance;

    InstanceMemory *mem;
    Span<uint8_t> old_stack_mem;
    Span<uint8_t> old_heap_mem;

    uint8_t *new_sp;
    uint8_t *old_sp;

    union {
        int8_t i8;
        uint8_t u8;
        int16_t i16;
        uint16_t u16;
        int32_t i32;
        uint32_t u32;
        int64_t i64;
        uint64_t u64;
        float f;
        double d;
        void *ptr;
        uint8_t buf[32];
    } result;
    uint8_t *return_ptr = nullptr;

    LocalArray<int16_t, 16> used_trampolines;
    HeapArray<OutArgument> out_arguments;

    BlockAllocator alloc;

public:
    CallData(Napi::Env env, InstanceData *instance, InstanceMemory *mem);
    ~CallData();

    void Dispose();

#if defined(UNITY_BUILD)
    #if defined(_MSC_VER)
        #define INLINE_IF_UNITY __forceinline
    #else
        #define INLINE_IF_UNITY __attribute__((always_inline)) inline
    #endif
#else
    #define INLINE_IF_UNITY
#endif

    INLINE_IF_UNITY bool Prepare(const FunctionInfo *func, const Napi::CallbackInfo &info);
    INLINE_IF_UNITY void Execute(const FunctionInfo *func, void *native);
    INLINE_IF_UNITY Napi::Value Complete(const FunctionInfo *func);

#undef INLINE_IF_UNITY

    void Relay(Size idx, uint8_t *sp);
    void RelayAsync(Size idx, uint8_t *sp);

    void DumpForward(const FunctionInfo *func) const;

    bool PushString(Napi::Value value, int directions, const char **out_str);
    bool PushString16(Napi::Value value, int directions, const char16_t **out_str16);
    bool PushString32(Napi::Value value, int directions, const char32_t **out_str32);
    Size PushStringValue(Napi::Value value, const char **out_str);
    Size PushString16Value(Napi::Value value, const char16_t **out_str16);
    Size PushString32Value(Napi::Value value, const char32_t **out_str32);
    bool PushObject(Napi::Object obj, const TypeInfo *type, uint8_t *origin);
    bool PushNormalArray(Napi::Array array, const TypeInfo *type, Size size, uint8_t *origin);
    void PushBuffer(Span<const uint8_t> buffer, const TypeInfo *type, uint8_t *origin);
    bool PushStringArray(Napi::Value value, const TypeInfo *type, uint8_t *origin);
    bool PushPointer(Napi::Value value, const TypeInfo *type, int directions, void **out_ptr);
    bool PushCallback(Napi::Value value, const TypeInfo *type, void **out_ptr);
    Size PushIndirectString(Napi::Array array, const TypeInfo *ref, uint8_t **out_ptr);

    void *ReserveTrampoline(const FunctionInfo *proto, Napi::Function func);

    template <typename T>
    bool AllocStack(Size size, Size align, T **out_ptr);
    template <typename T = uint8_t>
    T *AllocHeap(Size size, Size align);

    bool CheckDynamicLength(Napi::Object obj, Size element, const char *countedby, Napi::Value value);

    void PopOutArguments();
};

template <typename T>
inline bool CallData::AllocStack(Size size, Size align, T **out_ptr)
{
    uint8_t *ptr = AlignDown(mem->stack.end() - size, align);
    Size delta = mem->stack.end() - ptr;

    // Keep 512 bytes for redzone (required in some ABIs)
    if (mem->stack.len - 512 < delta) [[unlikely]] {
        ThrowError<Napi::Error>(env, "FFI call is taking up too much memory");
        return false;
    }

#if defined(K_DEBUG)
    MemSet(ptr, 0, delta);
#endif

    mem->stack.len -= delta;

    *out_ptr = (T *)ptr;
    return true;
}

template <typename T>
inline T *CallData::AllocHeap(Size size, Size align)
{
    uint8_t *ptr = AlignUp(mem->heap.ptr, align);
    Size delta = size + (ptr - mem->heap.ptr);

    if (size < 4096 && delta <= mem->heap.len) [[likely]] {
#if defined(K_DEBUG)
        MemSet(mem->heap.ptr, 0, delta);
#endif

        mem->heap.ptr += delta;
        mem->heap.len -= delta;

        return ptr;
    } else {
#if defined(K_DEBUG)
        int flags = (int)AllocFlag::Zero;
#else
        int flags = 0;
#endif

        ptr = (uint8_t *)AllocateRaw(&alloc, size + align, flags);
        ptr = AlignUp(ptr, align);

        return ptr;
    }
}

void PerformAsyncRelay(napi_env env, napi_value callback, void *ctx, void *udata);

void *GetTrampoline(int idx);

}
