// Copyright 2024 The IREE Authors
//
// Licensed under the Apache License v2.0 with LLVM Exceptions.
// See https://llvm.org/LICENSE.txt for license information.
// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception

// Must define _GNU_SOURCE before includes to get Dl_info/dladdr from dlfcn.h.
#define _GNU_SOURCE

#include "iree/base/target_platform.h"

#if defined(IREE_PLATFORM_APPLE)
#include <dlfcn.h>
#include <execinfo.h>
#define IREE_HAVE_BACKTRACE 1
#define IREE_STATUS_HAVE_STACK_TRACE_SUPPORT 1
#elif defined(IREE_PLATFORM_WINDOWS)
#pragma warning(disable : 4091)
#include <dbghelp.h>
#define IREE_STATUS_HAVE_STACK_TRACE_SUPPORT 1
#elif (defined(IREE_PLATFORM_ANDROID) || defined(IREE_PLATFORM_LINUX)) && \
    defined(IREE_HAVE_LIBBACKTRACE)
// Include our wrapper header which re-exports backtrace.h.
// This allows Bazel's layering check to see the header dependency.
#include <dlfcn.h>
#include <iree_libbacktrace.h>
#define IREE_USE_LIBBACKTRACE 1
#define IREE_STATUS_HAVE_STACK_TRACE_SUPPORT 1
#endif  // IREE_PLATFORM_*

#include <assert.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>

#include "iree/base/alignment.h"
#include "iree/base/allocator.h"
#include "iree/base/assert.h"
#include "iree/base/config.h"
#include "iree/base/printf.h"
#include "iree/base/status.h"
#include "iree/base/status_payload.h"
#include "iree/base/tracing.h"

#if defined(IREE_STATUS_HAVE_STACK_TRACE_SUPPORT) && \
    ((IREE_STATUS_FEATURES & IREE_STATUS_FEATURE_STACK_TRACE) != 0)

// TODO(benvanik): make a string_view utility to share with console tracing.
static iree_string_view_t iree_status_trim_file_path(const char* file_name) {
  size_t file_name_length = strlen(file_name);
  for (int i = (int)file_name_length - 1; i >= 0; --i) {
    char c = file_name[i];
    if (c == '/' || c == '\\') {
      return iree_make_string_view(file_name + i + 1, file_name_length - i - 1);
    }
  }
  return iree_make_string_view(file_name, file_name_length);
}

static iree_host_size_t iree_string_buffer_append_cstr(
    iree_host_size_t buffer_capacity, char* buffer,
    iree_host_size_t buffer_length, const char* str) {
  iree_host_size_t n =
      iree_snprintf(buffer ? buffer + buffer_length : NULL,
                    buffer ? buffer_capacity - buffer_length : 0, "%s", str);
  return IREE_UNLIKELY(n < 0) ? 0 : buffer_length + n;
}

static iree_host_size_t IREE_PRINTF_ATTRIBUTE(4, 5)
    iree_string_buffer_append_format(iree_host_size_t buffer_capacity,
                                     char* buffer,
                                     iree_host_size_t buffer_length,
                                     const char* format, ...) {
  va_list varargs;
  va_start(varargs, format);
  iree_host_size_t n = iree_vsnprintf(
      buffer ? buffer + buffer_length : NULL,
      buffer ? buffer_capacity - buffer_length : 0, format, varargs);
  va_end(varargs);
  return IREE_UNLIKELY(n < 0) ? 0 : buffer_length + n;
}

#if defined(IREE_USE_LIBBACKTRACE)

// pthread_once is used for thread-safe one-time initialization.
// We inline it here rather than using iree/base/threading/call_once.h because:
// 1. The :threading target has a circular dependency with :base.
// 2. Bare metal builds may not have pthreads, but libbacktrace requires it.
#include <pthread.h>

// Global backtrace state - initialized on first use.
static struct backtrace_state* iree_libbacktrace_state = NULL;
static pthread_once_t iree_libbacktrace_init_once = PTHREAD_ONCE_INIT;

static void iree_libbacktrace_error_callback(void* data, const char* msg,
                                             int errnum) {
  // Silently ignore errors - we don't want errors while reporting errors.
  (void)data;
  (void)msg;
  (void)errnum;
}

static void iree_libbacktrace_initialize(void) {
  iree_libbacktrace_state =
      backtrace_create_state(NULL,  // Let libbacktrace find the executable.
                             1,     // threaded = true
                             iree_libbacktrace_error_callback, NULL);
}

static struct backtrace_state* iree_libbacktrace_get_state(void) {
  pthread_once(&iree_libbacktrace_init_once, iree_libbacktrace_initialize);
  return iree_libbacktrace_state;
}

// Capture callback for backtrace_simple().
typedef struct {
  uintptr_t* addresses;
  int max_frames;
  int frame_count;
} iree_libbacktrace_capture_state_t;

static int iree_libbacktrace_capture_callback(void* data, uintptr_t pc) {
  iree_libbacktrace_capture_state_t* state =
      (iree_libbacktrace_capture_state_t*)data;
  if (state->frame_count >= state->max_frames) return 1;  // Stop.
  state->addresses[state->frame_count++] = pc;
  return 0;  // Continue.
}

// Format callback state for backtrace_pcinfo().
typedef struct {
  iree_host_size_t buffer_capacity;
  char* buffer;
  iree_host_size_t buffer_length;
  bool found;
} iree_libbacktrace_format_state_t;

static int iree_libbacktrace_pcinfo_callback(void* data, uintptr_t pc,
                                             const char* filename, int lineno,
                                             const char* function) {
  iree_libbacktrace_format_state_t* state =
      (iree_libbacktrace_format_state_t*)data;
  state->found = true;

  // Format: module <function+offset> (file:line)
  // We use dladdr for module name since libbacktrace doesn't provide it
  // directly in this callback.
  Dl_info info;
  if (dladdr((void*)pc, &info) != 0 && info.dli_fname) {
    iree_string_view_t fname = iree_status_trim_file_path(info.dli_fname);
    state->buffer_length = iree_string_buffer_append_format(
        state->buffer_capacity, state->buffer, state->buffer_length, "%.*s",
        (int)fname.size, fname.data);
  } else {
    state->buffer_length = iree_string_buffer_append_cstr(
        state->buffer_capacity, state->buffer, state->buffer_length, "???");
  }

  state->buffer_length = iree_string_buffer_append_cstr(
      state->buffer_capacity, state->buffer, state->buffer_length, " <");
  if (function) {
    state->buffer_length = iree_string_buffer_append_cstr(
        state->buffer_capacity, state->buffer, state->buffer_length, function);
  } else {
    state->buffer_length = iree_string_buffer_append_cstr(
        state->buffer_capacity, state->buffer, state->buffer_length, "???");
  }
  state->buffer_length = iree_string_buffer_append_cstr(
      state->buffer_capacity, state->buffer, state->buffer_length, ">");

  if (filename && lineno > 0) {
    iree_string_view_t file = iree_status_trim_file_path(filename);
    state->buffer_length = iree_string_buffer_append_format(
        state->buffer_capacity, state->buffer, state->buffer_length,
        " (%.*s:%d)", (int)file.size, file.data, lineno);
  }

  return 0;  // Continue (for inlined frames).
}

#endif  // IREE_USE_LIBBACKTRACE

#if defined(IREE_PLATFORM_WINDOWS)

typedef BOOL(WINAPI* PFN_SymInitialize)(HANDLE, PCSTR, BOOL);
typedef BOOL(WINAPI* PFN_SymCleanup)(HANDLE);
typedef BOOL(WINAPI* PFN_SymGetModuleInfo64)(HANDLE, DWORD64,
                                             PIMAGEHLP_MODULE64);
typedef BOOL(WINAPI* PFN_SymFromAddr)(HANDLE, DWORD64, PDWORD64, PSYMBOL_INFO);
typedef BOOL(WINAPI* PFN_SymGetLineFromAddr64)(HANDLE, DWORD64, PDWORD,
                                               PIMAGEHLP_LINE);
typedef struct iree_symbol_resolver_t {
  SRWLOCK mutex;
  HMODULE library;
  PFN_SymInitialize SymInitialize;
  PFN_SymCleanup SymCleanup;
  PFN_SymGetModuleInfo64 SymGetModuleInfo64;
  PFN_SymFromAddr SymFromAddr;
  PFN_SymGetLineFromAddr64 SymGetLineFromAddr64;
} iree_symbol_resolver_t;

// If tracy is enabled then it has its own dbghelp lock we need to use.
// If not enabled then we
#if defined(TRACY_ENABLE)
void IREEDbgHelpInit(void);
void IREEDbgHelpLock(void);
void IREEDbgHelpUnlock(void);
static void iree_symbol_resolver_initialize_mutex(
    iree_symbol_resolver_t* resolver) {
  IREEDbgHelpInit();
}
static void iree_symbol_resolver_lock(iree_symbol_resolver_t* resolver) {
  IREEDbgHelpLock();
}
static void iree_symbol_resolver_unlock(iree_symbol_resolver_t* resolver) {
  IREEDbgHelpUnlock();
}
#else
static void iree_symbol_resolver_initialize_mutex(
    iree_symbol_resolver_t* resolver) {
  InitializeSRWLock(&resolver->mutex);
}
static void iree_symbol_resolver_lock(iree_symbol_resolver_t* resolver) {
  AcquireSRWLockExclusive(&resolver->mutex);
}
static void iree_symbol_resolver_unlock(iree_symbol_resolver_t* resolver) {
  ReleaseSRWLockExclusive(&resolver->mutex);
}
#endif  // TRACY_ENABLE

static void iree_symbol_resolver_initialize(
    iree_symbol_resolver_t* out_resolver) {
  memset(out_resolver, 0, sizeof(*out_resolver));
  iree_symbol_resolver_initialize_mutex(out_resolver);
  out_resolver->library = LoadLibraryA("dbghelp.dll");
  if (!out_resolver->library) return;
  out_resolver->SymInitialize =
      (PFN_SymInitialize)GetProcAddress(out_resolver->library, "SymInitialize");
  out_resolver->SymCleanup =
      (PFN_SymCleanup)GetProcAddress(out_resolver->library, "SymCleanup");
  out_resolver->SymGetModuleInfo64 = (PFN_SymGetModuleInfo64)GetProcAddress(
      out_resolver->library, "SymGetModuleInfo64");
  out_resolver->SymFromAddr =
      (PFN_SymFromAddr)GetProcAddress(out_resolver->library, "SymFromAddr");
  out_resolver->SymGetLineFromAddr64 = (PFN_SymGetLineFromAddr64)GetProcAddress(
      out_resolver->library, "SymGetLineFromAddr64");
  if (!out_resolver->SymInitialize || !out_resolver->SymCleanup ||
      !out_resolver->SymGetModuleInfo64 || !out_resolver->SymFromAddr ||
      !out_resolver->SymGetLineFromAddr64) {
    FreeLibrary(out_resolver->library);
    memset(out_resolver, 0, sizeof(*out_resolver));
    return;
  }
  out_resolver->SymInitialize(GetCurrentProcess(), /*UserSearchPath=*/NULL,
                              /*fInvadeProcess=*/TRUE);
}

static void iree_symbol_resolver_deinitialize(
    iree_symbol_resolver_t* resolver) {
  if (resolver->SymCleanup) {
    resolver->SymCleanup(GetCurrentProcess());
  }
  if (resolver->library) {
    FreeLibrary(resolver->library);
    resolver->library = NULL;
  }
  memset(resolver, 0, sizeof(*resolver));
}

static BOOL CALLBACK iree_symbol_resolver_setup(PINIT_ONCE InitOnce,
                                                PVOID Parameter,
                                                PVOID* Context) {
  ((void)InitOnce);
  ((void)Context);
  iree_symbol_resolver_initialize((iree_symbol_resolver_t*)Parameter);
  return TRUE;
}

static INIT_ONCE instance_flag = INIT_ONCE_STATIC_INIT;
static iree_symbol_resolver_t instance;
static iree_symbol_resolver_t* iree_symbol_resolver_get(void) {
  InitOnceExecuteOnce(&instance_flag, iree_symbol_resolver_setup,
                      (PVOID)&instance, NULL);
  return &instance;
}

static bool iree_symbol_resolver_format_frame(iree_symbol_resolver_t* resolver,
                                              void* address,
                                              iree_host_size_t buffer_capacity,
                                              char* buffer,
                                              iree_host_size_t* buffer_length) {
  if (IREE_UNLIKELY(!resolver->library)) return false;

  HANDLE process = GetCurrentProcess();

  // Query generic information like what module the address is located in and
  // what the offset of the address is within its parent symbol.
  char symbol_buffer[512];
  SYMBOL_INFO* symbol_info = (SYMBOL_INFO*)symbol_buffer;
  symbol_info->SizeOfStruct = sizeof(*symbol_info);
  symbol_info->MaxNameLen = sizeof(symbol_buffer) - sizeof(*symbol_info);
  DWORD64 displacement64 = 0;
  iree_symbol_resolver_lock(resolver);
  BOOL result = resolver->SymFromAddr(process, (DWORD64)address,
                                      &displacement64, symbol_info);
  iree_symbol_resolver_unlock(resolver);
  if (!result) {
    // Failed to get any information; bail.
    return false;
  }

  IMAGEHLP_MODULE64 module;
  module.SizeOfStruct = sizeof(module);
  if (resolver->SymGetModuleInfo64(process, symbol_info->ModBase, &module)) {
    *buffer_length = iree_string_buffer_append_cstr(
        buffer_capacity, buffer, *buffer_length, module.ModuleName);
  } else {
    *buffer_length = iree_string_buffer_append_cstr(buffer_capacity, buffer,
                                                    *buffer_length, "???");
  }

  *buffer_length = iree_string_buffer_append_cstr(buffer_capacity, buffer,
                                                  *buffer_length, " <");
  if (symbol_info->NameLen > 0) {
    *buffer_length = iree_string_buffer_append_format(
        buffer_capacity, buffer, *buffer_length, "%.*s",
        (int)symbol_info->NameLen, symbol_info->Name);
  } else {
    *buffer_length = iree_string_buffer_append_cstr(buffer_capacity, buffer,
                                                    *buffer_length, "???");
  }
  if (displacement64) {
    *buffer_length = iree_string_buffer_append_format(
        buffer_capacity, buffer, *buffer_length, "+0x%0" PRIx64,
        displacement64);
  }
  *buffer_length = iree_string_buffer_append_cstr(buffer_capacity, buffer,
                                                  *buffer_length, ">");

  // Note that the returned name pointer is only valid while the lock is held.
  IMAGEHLP_LINE64 line;
  line.SizeOfStruct = sizeof(line);
  DWORD displacement32 = 0;
  iree_symbol_resolver_lock(resolver);
  if (resolver->SymGetLineFromAddr64(process, (DWORD64)address, &displacement32,
                                     &line)) {
    *buffer_length = iree_string_buffer_append_format(
        buffer_capacity, buffer, *buffer_length, " (%s:%" PRIu32 ")",
        line.FileName, (uint32_t)line.LineNumber);
  }
  iree_symbol_resolver_unlock(resolver);

  return true;
}

#endif  // IREE_PLATFORM_WINDOWS

static iree_host_size_t iree_status_payload_stack_trace_format_frame(
    void* address, iree_host_size_t buffer_capacity, char* buffer) {
  iree_host_size_t buffer_length = iree_string_buffer_append_format(
      buffer_capacity, buffer, 0, "  0x%016" PRIx64 " ",
      (uint64_t)(uintptr_t)address);
#if defined(IREE_HAVE_BACKTRACE)
  Dl_info info;
  if (dladdr(address, &info) != 0) {
    if (info.dli_fname) {
      iree_string_view_t fname = iree_status_trim_file_path(info.dli_fname);
      buffer_length = iree_string_buffer_append_format(
          buffer_capacity, buffer, buffer_length, "%.*s", (int)fname.size,
          fname.data);
    } else {
      buffer_length = iree_string_buffer_append_cstr(buffer_capacity, buffer,
                                                     buffer_length, "???");
    }
    buffer_length = iree_string_buffer_append_cstr(buffer_capacity, buffer,
                                                   buffer_length, " <");
    if (info.dli_sname) {
      iree_string_view_t sname = iree_make_cstring_view(info.dli_sname);
      buffer_length = iree_string_buffer_append_format(
          buffer_capacity, buffer, buffer_length, "%.*s", (int)sname.size,
          sname.data);
    } else {
      buffer_length = iree_string_buffer_append_cstr(buffer_capacity, buffer,
                                                     buffer_length, "???");
    }
    void* saddr = info.dli_saddr ? info.dli_saddr : address;
    ptrdiff_t diff = (ptrdiff_t)address - (ptrdiff_t)saddr;
    if (diff) {
      buffer_length = iree_string_buffer_append_format(
          buffer_capacity, buffer, buffer_length, "+0x%0" PRIx32,
          (int32_t)diff);
    }
    buffer_length = iree_string_buffer_append_cstr(buffer_capacity, buffer,
                                                   buffer_length, ">");
  } else {
    buffer_length = iree_string_buffer_append_cstr(buffer_capacity, buffer,
                                                   buffer_length, "???");
  }
#elif defined(IREE_PLATFORM_WINDOWS)
  if (!iree_symbol_resolver_format_frame(iree_symbol_resolver_get(), address,
                                         buffer_capacity, buffer,
                                         &buffer_length)) {
    buffer_length = iree_string_buffer_append_cstr(buffer_capacity, buffer,
                                                   buffer_length, "???");
  }
#elif defined(IREE_USE_LIBBACKTRACE)
  // Use libbacktrace for file:line:function info.
  struct backtrace_state* state = iree_libbacktrace_get_state();
  if (state) {
    iree_libbacktrace_format_state_t format_state = {
        .buffer_capacity = buffer_capacity,
        .buffer = buffer,
        .buffer_length = buffer_length,
        .found = false,
    };
    backtrace_pcinfo(state, (uintptr_t)address,
                     iree_libbacktrace_pcinfo_callback,
                     iree_libbacktrace_error_callback, &format_state);
    if (format_state.found) {
      buffer_length = format_state.buffer_length;
    } else {
      // pcinfo failed, fallback to dladdr for basic symbol info.
      Dl_info info;
      if (dladdr(address, &info) != 0 && info.dli_sname) {
        if (info.dli_fname) {
          iree_string_view_t fname = iree_status_trim_file_path(info.dli_fname);
          buffer_length = iree_string_buffer_append_format(
              buffer_capacity, buffer, buffer_length, "%.*s", (int)fname.size,
              fname.data);
        }
        buffer_length = iree_string_buffer_append_cstr(buffer_capacity, buffer,
                                                       buffer_length, " <");
        buffer_length = iree_string_buffer_append_cstr(
            buffer_capacity, buffer, buffer_length, info.dli_sname);
        buffer_length = iree_string_buffer_append_cstr(buffer_capacity, buffer,
                                                       buffer_length, ">");
      } else {
        buffer_length = iree_string_buffer_append_cstr(buffer_capacity, buffer,
                                                       buffer_length, "???");
      }
    }
  } else {
    buffer_length = iree_string_buffer_append_cstr(buffer_capacity, buffer,
                                                   buffer_length, "???");
  }
#else
  // Symbol resolution not implemented on the platform.
  buffer_length = iree_string_buffer_append_cstr(buffer_capacity, buffer,
                                                 buffer_length, "???");
#endif
  return iree_string_buffer_append_cstr(buffer_capacity, buffer, buffer_length,
                                        "\n");
}

static void iree_status_payload_stack_trace_formatter(
    const iree_status_payload_t* base_payload, iree_host_size_t buffer_capacity,
    char* buffer, iree_host_size_t* out_buffer_length) {
  iree_status_payload_stack_trace_t* payload =
      (iree_status_payload_stack_trace_t*)base_payload;
  if (payload->frame_count - payload->skip_frames == 0) return;
  iree_host_size_t buffer_length =
      iree_string_buffer_append_cstr(buffer_capacity, buffer, 0, "stack:\n");
  for (iree_host_size_t i = payload->skip_frames + 1; i < payload->frame_count;
       ++i) {
    buffer_length += iree_status_payload_stack_trace_format_frame(
        (void*)payload->addresses[i],
        buffer ? buffer_capacity - buffer_length : 0,
        buffer ? buffer + buffer_length : NULL);
    if (buffer_length > buffer_capacity) buffer = NULL;
  }
  *out_buffer_length = buffer_length;
}

// Captures the current stack and attaches it to the status storage.
// A count of |skip_frames| will be skipped from the top of the stack.
// Setting |skip_frames|=0 will include the caller in the stack while
// |skip_frames|=1 will exclude it.
iree_status_t iree_status_attach_stack_trace(iree_status_t status,
                                             iree_status_storage_t* storage,
                                             int skip_frames) {
  // Reserve storage for the number of stack frames so we can capture directly
  // into the storage even if we don't need them all. At the point we are
  // mallocing the exact size doesn't really matter.
  iree_status_payload_stack_trace_t* payload = NULL;
  iree_host_size_t total_size =
      sizeof(*payload) +
      sizeof(payload->addresses[0]) * IREE_STATUS_MAX_STACK_TRACE_FRAMES;

  iree_allocator_t allocator = iree_allocator_system();
  iree_status_ignore(
      iree_allocator_malloc(allocator, total_size, (void**)&payload));
  if (IREE_UNLIKELY(!payload)) return status;
  memset(payload, 0, sizeof(*payload));
  payload->header.type = IREE_STATUS_PAYLOAD_TYPE_STACK_TRACE;
  payload->header.allocator = allocator;
  payload->header.formatter = iree_status_payload_stack_trace_formatter;

#if defined(IREE_HAVE_BACKTRACE)
  // Capture up to the max frame count and skip some frames when formatting -
  // this means that our actual backtrace() max frame count is smaller than the
  // defined value. We could instead overallocate by skip_frames to waste a bit
  // of memory but keep the processing simpler.
  payload->skip_frames = skip_frames;
  payload->frame_count = backtrace((void**)&payload->addresses,
                                   IREE_STATUS_MAX_STACK_TRACE_FRAMES);
#elif defined(IREE_PLATFORM_WINDOWS)
  // NOTE: Win32 supports skip frames by default so we don't lose any storage.
  payload->frame_count =
      CaptureStackBackTrace(skip_frames, IREE_STATUS_MAX_STACK_TRACE_FRAMES,
                            (void**)&payload->addresses, NULL);
#elif defined(IREE_USE_LIBBACKTRACE)
  // Use libbacktrace's backtrace_simple for unwinding.
  struct backtrace_state* state = iree_libbacktrace_get_state();
  if (state) {
    iree_libbacktrace_capture_state_t capture_state = {
        .addresses = payload->addresses,
        .max_frames = IREE_STATUS_MAX_STACK_TRACE_FRAMES,
        .frame_count = 0,
    };
    payload->skip_frames = skip_frames;
    backtrace_simple(state, 0, iree_libbacktrace_capture_callback,
                     iree_libbacktrace_error_callback, &capture_state);
    payload->frame_count = capture_state.frame_count;
  }
#endif

  return iree_status_append_payload(status, storage,
                                    (iree_status_payload_t*)payload);
}

#else

iree_status_t iree_status_attach_stack_trace(iree_status_t status,
                                             iree_status_storage_t* storage,
                                             int skip_frames) {
  return status;
}

#endif  // has IREE_STATUS_FEATURE_STACK_TRACE
