/* +------------------------------------------------------------------------+
   |                     Mobile Robot Programming Toolkit (MRPT)            |
   |                          https://www.mrpt.org/                         |
   |                                                                        |
   | Copyright (c) 2005-2024, Individual contributors, see AUTHORS file     |
   | See: https://www.mrpt.org/Authors - All rights reserved.               |
   | Released under BSD License. See: https://www.mrpt.org/License          |
   +------------------------------------------------------------------------+ */

#include "core-precomp.h"  // Precompiled headers
//
#include <mrpt/config.h>  // MRPT_HAS_BFD, etc.
#include <mrpt/core/backtrace.h>
#include <mrpt/core/demangle.h>
#include <mrpt/core/format.h>
#include <mrpt/core/get_env.h>
#include <mrpt/core/winerror2str.h>

#include <iostream>
#include <mutex>

#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
//
#include <DbgHelp.h>
#else
#include <dlfcn.h>  // dladdr()
#include <dlfcn.h>
#ifndef __EMSCRIPTEN__
#include <execinfo.h>
#endif
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include <cstdlib>
#include <iostream>
#include <sstream>
#include <string>
#endif

#if MRPT_HAS_BFD && defined(_DEBUG)
// Partially based on code from:
// https://webcache.googleusercontent.com/search?q=cache:MXn9tpmIK5QJ:https://oroboro.com/printing-stack-traces-file-line/+&cd=2&hl=es&ct=clnk&gl=es

#include <bfd.h>   // in deb package: binutils-dev
#include <link.h>  // in deb package: libc6-dev

#if HAVE_DECL_BFD_GET_SECTION_FLAGS
#define mrpt_debug_bfd_section_flags(_abfd, _section) bfd_get_section_flags(_abfd, _section)
#elif HAVE_DECL_BFD_SECTION_FLAGS
#define mrpt_debug_bfd_section_flags(_abfd, _section) bfd_section_flags(_section)
#else
#error "Unsupported BFD API"
#endif

#if HAVE_DECL_BFD_GET_SECTION_VMA
#define mrpt_debug_bfd_section_vma(_abfd, _section) bfd_get_section_vma(_abfd, _section)
#elif HAVE_DECL_BFD_SECTION_VMA
#define mrpt_debug_bfd_section_vma(_abfd, _section) bfd_section_vma(_section)
#else
#error "Unsupported BFD API"
#endif

#if HAVE_1_ARG_BFD_SECTION_SIZE
#define mrpt_debug_bfd_section_size(_abfd, _section) bfd_section_size(_section)
#else
#define mrpt_debug_bfd_section_size(_abfd, _section) bfd_section_size(_abfd, _section);
#endif

using u32 = uint32_t;
using s32 = int32_t;

class FileMatch
{
 public:
  FileMatch(void* addr) noexcept : mAddress(addr) {}

  void* mAddress;
  const char* mFile = nullptr;
  void* mBase = nullptr;
};
static int findMatchingFile(struct dl_phdr_info* info, size_t /*size*/, void* data) noexcept
{
  FileMatch* match = (FileMatch*)data;

  for (u32 i = 0; i < info->dlpi_phnum; i++)
  {
    const ElfW(Phdr)& phdr = info->dlpi_phdr[i];

    if (phdr.p_type == PT_LOAD)
    {
      ElfW(Addr) vaddr = phdr.p_vaddr + info->dlpi_addr;
      ElfW(Addr) maddr = ElfW(Addr)(match->mAddress);
      if ((maddr >= vaddr) && (maddr < vaddr + phdr.p_memsz))
      {
        match->mFile = info->dlpi_name;
        match->mBase = (void*)info->dlpi_addr;
        return 1;
      }
    }
  }
  return 0;
}

static asymbol** kstSlurpSymtab(bfd* abfd, const char* fileName) noexcept
{
  if (!(bfd_get_file_flags(abfd) & HAS_SYMS))
  {
    printf("Error bfd file \"%s\" flagged as having no symbols.\n", fileName);
    return nullptr;
  }

  asymbol** syms;
  unsigned int size;

  long symcount = bfd_read_minisymbols(abfd, false, (void**)&syms, &size);
  if (symcount == 0) symcount = bfd_read_minisymbols(abfd, true, (void**)&syms, &size);

  if (symcount < 0)
  {
    printf("Error bfd file \"%s\", found no symbols.\n", fileName);
    return nullptr;
  }

  return syms;
}

class FileLineDesc
{
 public:
  FileLineDesc(asymbol** syms, bfd_vma pc) noexcept : mPc(pc), mSyms(syms) {}

  void findAddressInSection(bfd* abfd, asection* section) noexcept;

  bfd_vma mPc;
  char* mFilename = nullptr;
  char* mFunctionname = nullptr;
  unsigned int mLine;
  bool mFound = false;
  asymbol** mSyms;
};

void FileLineDesc::findAddressInSection(bfd* abfd, asection* section) noexcept
{
  if (mFound) return;

  if ((mrpt_debug_bfd_section_flags(abfd, section) & SEC_ALLOC) == 0) return;

  bfd_vma vma = mrpt_debug_bfd_section_vma(abfd, section);
  if (mPc < vma) return;

  bfd_size_type size = mrpt_debug_bfd_section_size(abfd, section);
  if (mPc >= (vma + size)) return;

  mFound = bfd_find_nearest_line(
      abfd, section, mSyms, (mPc - vma), (const char**)&mFilename, (const char**)&mFunctionname,
      &mLine);
}

static void findAddressInSection(bfd* abfd, asection* section, void* data) noexcept
{
  FileLineDesc* desc = (FileLineDesc*)data;
  if (!desc)
  {
    std::cerr << "[mrpt::callStackBackTrace()] ERROR: Cannot find debug "
                 "symbol.\n";
    return;
  }
  return desc->findAddressInSection(abfd, section);
}

std::vector<mrpt::TCallStackEntry> translateAddressesBuf(
    bfd* abfd, bfd_vma* addr, int numAddr, asymbol** syms) noexcept
{
  std::vector<mrpt::TCallStackEntry> ret;
  ret.resize(numAddr);

  for (int i = 0; i < numAddr; i++)
  {
    auto& e = ret.at(i);

    FileLineDesc desc(syms, addr[i]);

    bfd_map_over_sections(abfd, &findAddressInSection, (void*)&desc);

    if (!desc.mFound)
    {
      e.sourceFileName.clear();  // empty=no symbol name available
    }
    else
    {
      e.symbolNameOriginal = desc.mFunctionname;

      if (!e.symbolNameOriginal.empty()) e.symbolName = mrpt::demangle(e.symbolNameOriginal);

      if (desc.mFilename != nullptr)
      {
        char* h = strrchr(desc.mFilename, '/');
        if (h != nullptr) e.sourceFileName = h + 1;
        e.sourceFileFullPath = desc.mFilename;
      }
      e.sourceFileNumber = desc.mLine;
    }
  }

  return ret;
}

static std::vector<mrpt::TCallStackEntry> processFile(
    const char* fileName, bfd_vma* addr, int naddr, void* const PC_addr) noexcept
{
  bfd* abfd = bfd_openr(fileName, nullptr);
  if (!abfd)
  {
    printf("Error opening bfd file \"%s\"\n", fileName);
    return {};
  }

  if (bfd_check_format(abfd, bfd_archive))
  {
    printf("Cannot get addresses from archive \"%s\"\n", fileName);
    bfd_close(abfd);
    return {};
  }

  char** matching;
  if (!bfd_check_format_matches(abfd, bfd_object, &matching))
  {
    printf("Format does not match for archive \"%s\"\n", fileName);
    bfd_close(abfd);
    return {};
  }

  asymbol** syms = kstSlurpSymtab(abfd, fileName);
  if (!syms)
  {
    printf("Failed to read symbol table for archive \"%s\"\n", fileName);
    bfd_close(abfd);
    return {};
  }

  auto retBuf = translateAddressesBuf(abfd, addr, naddr, syms);
  for (auto& e : retBuf) e.address = PC_addr;

  free(syms);

  bfd_close(abfd);
  return retBuf;
}

static void mrpt_bfd_error_handler_type(const char*, va_list)
{
  // do nothing?
}

static std::vector<mrpt::TCallStackEntry> backtraceSymbols(
    void* const* addrList, int numAddr) noexcept
{
  // initialize the bfd library
  bfd_init();

  bfd_set_error_handler(&mrpt_bfd_error_handler_type);

  std::vector<mrpt::TCallStackEntry> ret;
  for (int idx = 0; idx < numAddr; idx++)
  {
    // find which executable, or library the symbol is from
    void* const PC_addr = addrList[idx];
    FileMatch match(PC_addr);
    dl_iterate_phdr(findMatchingFile, &match);

    // adjust the address in the global space of your binary to an
    // offset in the relevant library
    bfd_vma addr = (bfd_vma)(addrList[idx]);
    addr -= (bfd_vma)(match.mBase);

    // lookup the symbol
    const char* fil = (match.mFile && strlen(match.mFile)) ? match.mFile : "/proc/self/exe";

    auto newVals = processFile(fil, &addr, 1, PC_addr);
    for (auto& v : newVals) ret.emplace_back(std::move(v));
  }

  return ret;
}
#endif

#ifdef _WIN32
class WindowsSymResources
{
 public:
  static WindowsSymResources& Instance()
  {
    static WindowsSymResources inst;
    return inst;
  }

  bool initialized() const { return m_init_ok; }
  HANDLE processHandle() const { return m_hProcess; }

 private:
  const HANDLE m_hProcess = GetCurrentProcess();
  bool m_init_ok = false;

  WindowsSymResources()
  {
    SymSetOptions(SYMOPT_UNDNAME | SYMOPT_DEFERRED_LOADS);

    if (!SymInitialize(m_hProcess, nullptr /* UserSearchPath  */, TRUE /*fInvadeProcess*/))
    {
      std::cerr << mrpt::winerror2str("mrpt::callStackBackTrace", " Error in SymInitialize(): ")
                << std::endl;
      return;
    }
    m_init_ok = true;
  }
  ~WindowsSymResources()
  {
    m_init_ok = false;
    SymCleanup(m_hProcess);
  }
};

#endif

void mrpt::callStackBackTrace(
    TCallStackBackTrace& out_bt,
    const unsigned int framesToSkip,
    const unsigned int framesToCapture) noexcept
{
  out_bt.backtrace_levels.clear();

#ifdef _WIN32
  std::vector<void*> backTrace(framesToCapture + 4);

  // Auto initialize Sym system on first call, auto free at program end.
  auto& sym = WindowsSymResources::Instance();
  if (!sym.initialized()) return;  // error already dumped to std::cerr inside the ctor

  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;

  const USHORT nFrames =
      CaptureStackBackTrace(framesToSkip, framesToCapture, backTrace.data(), nullptr);
  for (unsigned int i = 0; i < nFrames; i++)
  {
    TCallStackEntry cse;
    cse.address = backTrace[i];

    if (!SymFromAddr(sym.processHandle(), (DWORD64)cse.address, nullptr, pSymbol))
    {
      cse.symbolName = "???";
      cse.symbolNameOriginal = "???";
      out_bt.backtrace_levels.emplace_back(cse);
      continue;
    }
    SYMBOL_INFO& si = *pSymbol;

    cse.symbolNameOriginal = si.Name;
    cse.symbolName = mrpt::demangle(si.Name);

    out_bt.backtrace_levels.emplace_back(cse);
  }
#elif !defined(__EMSCRIPTEN__)
  // Based on: https://gist.github.com/fmela/591333
  std::vector<void*> callstack(framesToCapture + 4);
  const int nMaxFrames = framesToCapture;
  int nFrames = ::backtrace(callstack.data(), nMaxFrames);
  char** symbols = ::backtrace_symbols(callstack.data(), nFrames);

// Detailed callback stack traces with debug symbols: only in debug builds:
#if MRPT_HAS_BFD && defined(_DEBUG)
  static const bool MRPT_BACKTRACE_DISABLE_BFD =
      mrpt::get_env<bool>("MRPT_BACKTRACE_DISABLE_BFD", false);
  const bool use_bfd = !MRPT_BACKTRACE_DISABLE_BFD;
#else
  const bool use_bfd = false;
#endif

  if (use_bfd)
  {
    // Use BFD to solve for symbol names and line numbers.
    std::vector<void*> addrs;
    for (int i = (int)framesToSkip; i < nFrames; i++) addrs.push_back(callstack[i]);

#if MRPT_HAS_BFD && defined(_DEBUG)
    // It seems BFD crashes if it is invoked from several threads in
    // parallel!
    static std::mutex bfdMtx;
    {
      std::lock_guard<std::mutex> lck(bfdMtx);
      out_bt.backtrace_levels = backtraceSymbols(addrs.data(), addrs.size());
    }
#else
    std::cerr << "[mrpt::callStackBackTrace] Should never reach here!!\n";
#endif
  }
  else
  {
    // If BFD is not available, solve for symbol names only:
    for (int i = (int)framesToSkip; i < nFrames; i++)
    {
      TCallStackEntry cse;
      cse.address = callstack[i];

      Dl_info info;
      if (dladdr(callstack[i], &info) && info.dli_sname)
      {
        cse.symbolNameOriginal = info.dli_sname == nullptr ? symbols[i] : info.dli_sname;
        cse.symbolName = mrpt::demangle(cse.symbolNameOriginal);
      }
      out_bt.backtrace_levels.emplace_back(cse);
    }
  }

  free(symbols);
#else
  // Emscripten: do nothing.
  return;
#endif
}

mrpt::TCallStackBackTrace::TCallStackBackTrace() = default;
std::string mrpt::TCallStackBackTrace::asString() const noexcept
{
  static const bool MRPT_BACKTRACE_PRINT_ADDRESS =
      mrpt::get_env<bool>("MRPT_BACKTRACE_PRINT_ADDRESS", false);

  std::string trace_buf;
  for (std::size_t i = 0; i < backtrace_levels.size(); i++)
  {
    const auto& bt = backtrace_levels[i];

    trace_buf += mrpt::format("[%-2d] ", static_cast<int>(i));

    if (MRPT_BACKTRACE_PRINT_ADDRESS || bt.symbolName.empty())
    {
      trace_buf += mrpt::format("%*p ", int(2 + sizeof(void*) * 2), bt.address);
    }

    if (!bt.symbolName.empty())
    {
      if (!bt.sourceFileName.empty())
      {
        trace_buf += bt.sourceFileFullPath;
        trace_buf += ":";
        trace_buf += std::to_string(bt.sourceFileNumber);
        trace_buf += " ";
      }
      else
      {
        trace_buf += "(unknown file) ";
      }

      trace_buf += bt.symbolName;
    }
    trace_buf += "\n";
  }
  return trace_buf;
}
