#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <time.h>
#include <pthread.h>
#include <unistd.h>
#include <sys/syscall.h>
#include <dlfcn.h>
#include <backtrace.h>

extern int opt_trace;

void __cyg_profile_func_enter(void *this_fn, void *call_site) __attribute__((no_instrument_function));
void __cyg_profile_func_exit(void *this_fn, void *call_site) __attribute__((no_instrument_function));

static __thread int tracedepth = 0;
static __thread int in_tracer = 0; /* Guard against recursive instrumentation */

static FILE *tracefile = NULL;
static struct backtrace_state *bt_state = NULL;

static void init_backtrace(void) __attribute__((no_instrument_function));
static void init_backtrace(void) {
    if (!bt_state) {
        bt_state = backtrace_create_state(NULL, 1, NULL, NULL);
        tracefile = fopen("TRACE.log", "w");
    }
}

typedef struct {
    const char *filename;
    int lineno;
    const char *function;
} loc_info_t;

static int line_spec_callback(void *data, uintptr_t pc, const char *filename, int lineno, const char *function) __attribute__((no_instrument_function));
static int line_spec_callback(void *data, uintptr_t pc, const char *filename, int lineno, const char *function) {
    loc_info_t *info = (loc_info_t *)data;
    if (filename) {
      info->filename = filename;
      if (strrchr(filename, '/') != NULL) info->filename = strrchr(filename, '/')+1; 
    }
    info->lineno = lineno;
    if (function) info->function = function;
    return 0;
}

static void error_callback(void *data, const char *msg, int errnum) __attribute__((no_instrument_function));
static void error_callback(void *data, const char *msg, int errnum) {
    /* Fallback on error */
}

static void get_symbol_info(void *addr, char *buf, size_t buf_len, loc_info_t *loc) __attribute__((no_instrument_function));
#ifndef USE_NEWER
static void get_symbol_info(void *addr, char *buf, size_t buf_len, loc_info_t *loc) {
    loc->filename = "??";
    loc->lineno = 0;
    loc->function = "unknown";

    if (bt_state) {
        backtrace_pcinfo(bt_state, (uintptr_t)addr, line_spec_callback, error_callback, loc);
    }

    const char *sym_name = loc->function;
    Dl_info dl_info;
    if ((!sym_name || strcmp(sym_name, "unknown") == 0) && dladdr(addr, &dl_info) && dl_info.dli_sname) {
        sym_name = dl_info.dli_sname;
    }

    snprintf(buf, buf_len, "%s", sym_name ? sym_name : "unknown");
}
#else
static void get_symbol_info(void *addr, char *buf, size_t buf_len, loc_info_t *loc) {
  loc->filename = "??";
  loc->lineno = 0;
  loc->function = "unknown";

  uintptr_t pc = (uintptr_t)addr;

  Dl_info dl_info;
  if (dladdr(addr, &dl_info)) {
    if (dl_info.dli_sname) {
      loc->function = dl_info.dli_sname;
    }
    /* Adjust for PIE / ASLR base offset if dli_fbase is valid */
    if (dl_info.dli_fbase) {
      pc = (uintptr_t)addr - (uintptr_t)dl_info.dli_fbase;
    }
  }

  if (bt_state) {
    backtrace_pcinfo(bt_state, pc, line_spec_callback, error_callback, loc);
  }

  snprintf(buf, buf_len, "%s", loc->function ? loc->function : "unknown");
}
#endif

void __cyg_profile_func_enter(void *this_fn, void *call_site) {
    if (!opt_trace) return;
    if (in_tracer) return;
    in_tracer = 1;

    init_backtrace();

    char fn_buf[256];
    char caller_buf[256];
    loc_info_t fn_loc, caller_loc;

    get_symbol_info(this_fn, fn_buf, sizeof(fn_buf), &fn_loc);
    get_symbol_info(call_site, caller_buf, sizeof(caller_buf), &caller_loc);

    struct timespec ts;
    clock_gettime(CLOCK_MONOTONIC, &ts);

    /*long tid =*/(void) syscall(SYS_gettid);

    //int indent = tracedepth < 40 ? tracedepth : 40;
    int indent = tracedepth < 4 ? tracedepth : 4;

    if (tracefile) fprintf(tracefile, /*"[%6ld.%06ld] [TID %ld] "*/"%*s> Entered %s() (in %s) starting at line %d, called from %s() line %d of %s]\n",
            //ts.tv_sec % 1000000, ts.tv_nsec / 1000,
            //tid,
            indent * 2, "",
            fn_buf,
            fn_loc.filename, fn_loc.lineno,
            caller_buf,
            caller_loc.lineno, caller_loc.filename);
    if (tracefile) fflush(tracefile);

    tracedepth++;
    in_tracer = 0;
}

void __cyg_profile_func_exit(void *this_fn, void *call_site) {
    if (!opt_trace) return;
    if (in_tracer) return;
    in_tracer = 1;

    if (tracedepth > 0) tracedepth--;

    char fn_buf[256];
    loc_info_t fn_loc;
    get_symbol_info(this_fn, fn_buf, sizeof(fn_buf), &fn_loc);

    struct timespec ts;
    clock_gettime(CLOCK_MONOTONIC, &ts);

    /*long tid =*/(void) syscall(SYS_gettid);

    //int indent = tracedepth < 40 ? tracedepth : 40;
    int indent = tracedepth < 4 ? tracedepth : 4;

    if (tracefile) fprintf(tracefile, /*"[%6ld.%06ld] [TID %ld] "*/"%*s< %s()\n",
            //ts.tv_sec % 1000000, ts.tv_nsec / 1000,
            //tid,
            indent * 2, "",
            fn_buf);
    if (tracefile) fflush(tracefile);
    in_tracer = 0;
}

#ifdef DBMAIN
void my_procedure(void) {
    printf("Inside my_procedure\n");
}

int main(void) {
    printf("In main\n");
    my_procedure();
    return 0;
}
#endif
