#include <stdio.h>
#include <cxxabi.h>
#include <stdlib.h>
#include <errno.h>

#include <l4/libunwind/backtrace_util.h>

#include "libunwind-l4.h"
#include "os-l4.h"
#include "elf-image-cache.h"

static char *demangle(const char *function)
{
  int status = -1;
  char *demfunc = 0;
  demfunc = __cxa_demangle(function, 0, 0, &status);
  if (status == 0)
    return demfunc;
  return NULL;
}

typedef struct backtrace_cb_data
{
  int counter;
  elf_image_handle_t *elf;
  unsigned long segbase;
  unsigned long mapoff;
  char path[256];
  unsigned long vbase;
} backtrace_cb_data_t;

static void
print_func_name(const char *function)
{
  char *demfunc = demangle(function);
  if (demfunc)
    {
      printf("%s", demfunc);
      free(demfunc);
    }
  else
    printf("%s()", function);
}

static int
bt_cb_one(void *vdata, uintptr_t pc,
          const char *filename, int lineno, const char *function)
{
  backtrace_cb_data_t *data = vdata;

  unw_word_t ip = (unw_word_t)pc - data->vbase + data->segbase;
  printf("#%d 0x%08lx", data->counter, (l4_addr_t)ip);

  // Print module and relative virtual address in module
  if (data->path[0])
    printf(" = 0x%lx@%s", (l4_addr_t)pc, data->path);

  printf(": ");
  if (function)
    print_func_name(function);
  else
    {
      char func_name[256];
      unw_word_t off;
      // Fallback to symbol name from (dynamic linker) symbol table.
      if (data->elf &&
          // get_proc_name_in_image(), or rather get_load_offset() which is used
          // internally, expects mapoff to be page size aligned.
          elf_w(get_proc_name_in_image)(unw_local_addr_space, &data->elf->ei,
                                        data->segbase & L4_PAGEMASK,
                                        ip, func_name, sizeof(func_name),
                                        &off) >= 0)
        {
          print_func_name(func_name);
          printf("+0x%lx", (unsigned long)off);
        }
      else
        printf("unknown");
    }

  if (filename || lineno)
    printf(" at %s:%d", filename, lineno);
  printf("\n");
  data->counter++;
  return 1;
}

static void
bt_err(void *vdata, const char *msg, int errnum)
{
  backtrace_cb_data_t *data = vdata;
  (void) data; (void) msg; (void) errnum;
  Debug(1, "#%d backtrace-error: %s (%d)\n", data->counter, msg, errnum);
}

static int
do_backtrace_pcinfo(unw_word_t ip, backtrace_cb_data_t *data)
{
  if (!data->elf)
    return 0;

  // Get relocation offset... (use get_load_offset...)
  Elf_W(Ehdr) *ehdr = data->elf->ei.image;
  const ElfW(Phdr) *phdr = (Elf_W(Phdr) *) ((char *) data->elf->ei.image + ehdr->e_phoff);

  /* See if PC falls into one of the loaded segments.*/
  // TODO: Use get_load_offset?!
  data->vbase = 0;
  for (int n = 0; n < ehdr->e_phnum; n++, phdr++)
    {
      if (phdr->p_type == PT_LOAD && phdr->p_offset == data->mapoff)
        {
          data->vbase = phdr->p_vaddr;
          break;
        }
    }

  struct backtrace_state *state = get_backtrace_state(data->elf, bt_err, data);
  // For relocated binaries ip has to be adjusted to the default
  // virtual address, otherwise pcinfo will find nothing!
  return state && backtrace_pcinfo(state, ip - data->segbase + data->vbase,
                                   bt_cb_one, bt_err, data) == 1;
}

static void do_backtrace(unw_cursor_t *cursor, pid_t pid)
{
  unw_word_t ip;

  backtrace_cb_data_t data;
  memset(&data, 0, sizeof(data));
  data.counter = 0;
  do
    {
      unw_get_reg(cursor, UNW_REG_IP, &ip);

      if (l4_find_elf(pid_to_l4(pid), ip, &data.segbase, &data.mapoff,
                      data.path, sizeof(data.path)) >= 0)
        {
          data.elf = get_elf_image(data.path);
          if (do_backtrace_pcinfo(ip, &data))
            // Success...
            continue;
        }
      else
        {
          data.elf = NULL;
          data.segbase = 0;
          data.mapoff = 0;
          data.path[0] = 0;
          data.vbase = 0;
        }

      // If unable to find binary or backtrace_pcinfo failed, invoke bt_cb_one manually.
      bt_cb_one(&data, ip - data.segbase + data.vbase, NULL, 0, NULL);
    }
  while (unw_step (cursor) > 0);
}

void libunwind_do_local_backtrace(void)
{
  unw_cursor_t cursor;
  unw_context_t uc;

  unw_getcontext(&uc);
  int r = unw_init_local(&cursor, &uc);
  if (r)
    return;

  do_backtrace(&cursor, getpid());
}

// TODO: Fall back on binary if the region manager does not provide info?
void libunwind_do_remote_backtrace(l4_exc_regs_t *regs, l4_cap_idx_t rm,
                                   libunwind_access_remote_mem_f remote_mem,
                                   void *remote_mem_arg)
{
  unw_cursor_t cursor;

  l4_unw_data_t data;
  data.regs = regs;
  data.rm = rm;
  data.remote_mem = remote_mem;
  data.remote_mem_arg  = remote_mem_arg;

  unw_addr_space_t as = unw_create_addr_space(&l4_unw_accessors, 0);
  int r = unw_init_remote(&cursor, as, &data);
  if (r)
    return;

  do_backtrace(&cursor, l4_to_pid(rm));
}
