/*
 * Copyright 2025 Rive
 */

#include "common/stacktrace.hpp"
#include <assert.h>

#if defined(NO_REDIRECT_OUTPUT) || defined(__EMSCRIPTEN__)

namespace stacktrace
{
void replace_signal_handlers(SignalFunc signalFunc,
                             ExitFunc atExitFunc) noexcept
{}
}; // namespace stacktrace

#else

#ifdef _WIN32
#include <Windows.h>
#include <dbghelp.h>
#else
#include <inttypes.h>
#ifdef RIVE_ANDROID
#include <unwind.h>
#include <android/log.h>
#endif
#include <execinfo.h>
#include <string.h>
#include <errno.h>
#include <unistd.h>
#include <dlfcn.h>
#include <cxxabi.h>
#endif

#ifdef SYS_SIGNAL_H
#include <sys/signal.h>
#else
#include <signal.h>
#endif

#include <stdio.h>
#include <sstream>

static SignalFunc signalFunc;
static int signalRecurseLevel = 0;

// this is outside the namespace so we can use it as if it was defined by the
// c++ runtime
#if defined(SYS_SIGNAL_H) || defined(_WIN32)
const char* strsignal(int signo)
{
    switch (signo)
    {
        case SIGINT:
            return "SIGINT";
        case SIGILL:
            return "SIGILL";
        case SIGFPE:
            return "SIGFPE";
        case SIGSEGV:
            return "SIGSEGV";
        case SIGTERM:
            return "SIGTERM";
        case SIGBREAK:
            return "SIGBREAK";
        case SIGABRT:
            return "SIGABRT";
    }
    return "Unknown Signal";
}
#endif

namespace stacktrace
{
#if defined(_WIN32)
LONG WINAPI top_level_exception_handler(PEXCEPTION_POINTERS pExceptionInfo)
{
    std::stringstream f;

    HANDLE process = GetCurrentProcess();
    SymInitialize(process, NULL, TRUE);

    // StackWalk64() may modify context record passed to it, so we will
    // use a copy.
    CONTEXT context_record = *pExceptionInfo->ContextRecord;
    // Initialize stack walking.
    STACKFRAME64 stack_frame;
    memset(&stack_frame, 0, sizeof(stack_frame));
#if defined(_WIN64)
    int machine_type = IMAGE_FILE_MACHINE_AMD64;
    stack_frame.AddrPC.Offset = context_record.Rip;
    stack_frame.AddrFrame.Offset = context_record.Rbp;
    stack_frame.AddrStack.Offset = context_record.Rsp;
#else
    int machine_type = IMAGE_FILE_MACHINE_I386;
    stack_frame.AddrPC.Offset = context_record.Eip;
    stack_frame.AddrFrame.Offset = context_record.Ebp;
    stack_frame.AddrStack.Offset = context_record.Esp;
#endif
    stack_frame.AddrPC.Mode = AddrModeFlat;
    stack_frame.AddrFrame.Mode = AddrModeFlat;
    stack_frame.AddrStack.Mode = AddrModeFlat;

    char buffer[sizeof(SYMBOL_INFO) + MAX_SYM_NAME * sizeof(TCHAR)];
    PSYMBOL_INFO pSymbol = (PSYMBOL_INFO)buffer;

    pSymbol->SizeOfStruct = sizeof(SYMBOL_INFO);
    pSymbol->MaxNameLen = MAX_SYM_NAME;

    while (StackWalk64(machine_type,
                       GetCurrentProcess(),
                       GetCurrentThread(),
                       &stack_frame,
                       &context_record,
                       NULL,
                       &SymFunctionTableAccess64,
                       &SymGetModuleBase64,
                       NULL))
    {

        DWORD64 displacement = 0;

        if (!SymFromAddr(process,
                         (DWORD64)stack_frame.AddrPC.Offset,
                         &displacement,
                         pSymbol))
        {
            continue;
        }

        IMAGEHLP_MODULE64 moduleInfo;
        ZeroMemory(&moduleInfo, sizeof(IMAGEHLP_MODULE64));
        moduleInfo.SizeOfStruct = sizeof(moduleInfo);

        if (::SymGetModuleInfo64(process, pSymbol->ModBase, &moduleInfo))
            f << moduleInfo.ModuleName << ": ";

        f << pSymbol->Name << " + 0x" << std::hex << displacement << std::endl;
    }

    signalFunc(f.str().c_str());

    // match to SymInitialize
    SymCleanup(process);

    return EXCEPTION_CONTINUE_SEARCH;
}

void build_stack(int signo)
{
    CONTEXT context;
    ZeroMemory(&context, sizeof(CONTEXT));
    RtlCaptureContext(&context);
    EXCEPTION_POINTERS exceptionPointers;
    ZeroMemory(&exceptionPointers, sizeof(EXCEPTION_POINTERS));
    exceptionPointers.ContextRecord = &context;
    top_level_exception_handler(&exceptionPointers);
}

static void handle_signal(int signo) noexcept
{
    // this is because we can accidently spam the signal handler if we can't
    // communicate with the server
    if (signo == SIGABRT)
    {
        if (++signalRecurseLevel > 1)
        {
            return;
        }
    }

    printf("Received signal %i (\"%s\")\n", signo, strsignal(signo));

    build_stack(signo);
    // set the signal handler to the original
    signal(signo, SIG_DFL);

#ifdef _WIN32
    if (IsDebuggerPresent())
    {
        // Cause the debugger to break
        DebugBreak();
    }
#endif

    // re raise the signal
    raise(signo);
}

void replace_signal_handlers(SignalFunc inSignalFunc,
                             ExitFunc atExitFunc) noexcept
{
    assert(inSignalFunc);
    assert(atExitFunc);

    // signals can only use global vars
    signalFunc = inSignalFunc;

    SetUnhandledExceptionFilter(top_level_exception_handler);

    for (int i = 1; i <= SIGTERM; ++i)
    {
        signal(i, handle_signal);
    }
    // for windows SIGABRT is a higher number then SIGTERM, so make sure to
    // intercept it as well
    signal(SIGABRT, handle_signal);

    atexit(atExitFunc);
}
#else
static void get_symbol_line(void* instructionPointer,
                            uint32_t i,
                            char* buffer,
                            std::size_t bufferSize)
{
    Dl_info dlInfo;
    if (dladdr(instructionPointer, &dlInfo) && dlInfo.dli_sname != nullptr)
    {
        char* demangled = nullptr;
        int status;
        demangled = abi::__cxa_demangle(dlInfo.dli_sname, nullptr, 0, &status);
        snprintf(buffer,
                 bufferSize,
                 "%u %s\n",
                 i,
                 (status == 0 && demangled != nullptr) ? demangled
                                                       : dlInfo.dli_sname);
        free(demangled);
    }
    else
    {
        snprintf(buffer,
                 bufferSize,
                 "%u <unknown symbol @%p>)\n",
                 i,
                 instructionPointer);
    }

    // snprintf does not null terminate if we hit the limit so add a null
    // terminator here to be safe.
    buffer[bufferSize - 1] = '\0';
}

template <std::size_t N>
static void get_symbol_line(void* instructionPointer,
                            uint32_t i,
                            char (&buffer)[N])
{
    get_symbol_line(instructionPointer, i, buffer, N);
}

#ifdef RIVE_ANDROID
#define LOG(...)                                                               \
    __android_log_print(ANDROID_LOG_ERROR, "rive_android_tests", __VA_ARGS__)
static void handle_signal(int sigNum, siginfo_t* signalInfo, void* userContext)
{
    LOG("Received signal %i (\"%s\")\n", sigNum, strsignal(sigNum));

    LOG("Stack trace:\n");
    struct State
    {
        uint32_t i = 0;
        std::stringstream f;
    };

    State st{};
    _Unwind_Backtrace(
        [](struct _Unwind_Context* context, void* stateVoid) {
            auto state = static_cast<State*>(stateVoid);
            void* ip = reinterpret_cast<void*>(_Unwind_GetIP(context));
            if (ip != nullptr)
            {
                char buffer[1024];
                get_symbol_line(ip, state->i, buffer);
                LOG("%s", buffer);
                state->f << buffer;
            }
            state->i++;
            return _URC_NO_REASON;
        },
        &st);

    signal(sigNum, SIG_DFL);
    signalFunc(st.f.str().c_str());
    raise(sigNum);
}

#else // we are unix

static void handle_signal(int sigNum, siginfo_t* signalInfo, void* userContext)
{
    printf("Received signal %i (\"%s\")\n", sigNum, strsignal(sigNum));
    // this is because we can accidently spam the signal handler if we can't
    // communicate with the server
    if (sigNum == SIGABRT)
    {
        if (++signalRecurseLevel > 1)
        {
            return;
        }
    }

    std::stringstream f;

    void* stacktrace[1024];
    char** symbols;
    char stringBuff[1024];
    // this is technically unsafe since it uses malloc behind the scene but it's
    // way simpler then the alternative and this is just for testing anyway so
    // it's fine
    int numFrames = backtrace(stacktrace, 1024);
    symbols = backtrace_symbols(stacktrace, numFrames);
    for (int i = 1; i < numFrames; i++)
    {
        get_symbol_line(stacktrace[i], i, stringBuff, std::size(stringBuff));
        f << stringBuff << symbols[i] << "\n";
    }

    free(symbols);

    // send stack to server
    signalFunc(f.str().c_str());

    exit(sigNum);
}
#endif
void replace_signal_handlers(SignalFunc inSignalFunc,
                             ExitFunc atExitFunc) noexcept
{
    assert(inSignalFunc);
    assert(atExitFunc);

    signalFunc = inSignalFunc;

    struct sigaction action;
    struct sigaction oldAction;
    action.sa_flags = SA_SIGINFO | SA_ONSTACK;
#if defined(__LP64__) && defined(__APPLE__)
    action.sa_flags |= SA_64REGSET;
#endif
    action.sa_sigaction = &handle_signal;

    for (int i = 1; i <= SIGTERM; ++i)
    {
        sigaction(i, &action, &oldAction);
    }

    atexit(atExitFunc);
}

#endif // defined _WIN32

}; // namespace stacktrace
#endif // defined(NO_REDIRECT_OUTPUT) || defined(__EMSCRIPTEN__)
