#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <unistd.h>
#include <sys/mman.h>

#include "frida-gumjs.h"

#include "config.h"
#include "entry.h"
#include "shm.h"
#include "stalker.h"
#include "stats.h"
#include "util.h"

#define MICRO_TO_SEC 1000000

char                *stats_filename = NULL;
guint64              stats_interval = 0;
static guint64       stats_interval_us = 0;
static int           stats_fd = -1;
static stats_data_t *stats_data = MAP_FAILED;

void stats_write(void) {

  if (stats_filename == NULL) { return; }

  if (stats_interval == 0) { return; }

  guint64 current_time = g_get_monotonic_time();
  if ((current_time - stats_data->prev.stats_time) < stats_interval_us) {

    return;

  }

  IGNORED_RETURN(ftruncate(stats_fd, 0));
  IGNORED_RETURN(lseek(stats_fd, 0, SEEK_SET));

  stats_data->curr.stats_time = current_time;

  GDateTime *date_time = g_date_time_new_now_local();
  char      *date_string = g_date_time_format(date_time, "%Y-%m-%d");
  char      *time_string = g_date_time_format(date_time, "%H:%M:%S");
  guint elapsed = (stats_data->curr.stats_time - stats_data->prev.stats_time) /
                  MICRO_TO_SEC;

  stats_print("stats\n");
  stats_print("-----\n");

  stats_print("%-21s %s %s\n", "Time", date_string, time_string);
  stats_print("%-30s %10u seconds \n", "Elapsed", elapsed);

  stats_print("\n");
  stats_print("\n");

  g_free(time_string);
  g_free(date_string);
  g_date_time_unref(date_time);

  stats_write_arch(stats_data);

  memcpy(&stats_data->prev, &stats_data->curr, sizeof(stats_t));

}

static void gum_afl_stalker_stats_increment_total(
    GumStalkerObserver *observer) {

  UNUSED_PARAMETER(observer);

  if (!entry_compiled) { return; }
  stats_data->curr.total++;

}

static void gum_afl_stalker_stats_increment_call_imm(
    GumStalkerObserver *observer) {

  UNUSED_PARAMETER(observer);

  if (!entry_compiled) { return; }
  stats_data->curr.call_imm++;

}

static void gum_afl_stalker_stats_increment_call_reg(
    GumStalkerObserver *observer) {

  UNUSED_PARAMETER(observer);

  if (!entry_compiled) { return; }
  stats_data->curr.call_reg++;

}

static void gum_afl_stalker_stats_increment_call_mem(
    GumStalkerObserver *observer) {

  UNUSED_PARAMETER(observer);

  if (!entry_compiled) { return; }
  stats_data->curr.call_mem++;

}

static void gum_afl_stalker_stats_increment_excluded_call_reg(
    GumStalkerObserver *observer) {

  UNUSED_PARAMETER(observer);

  if (!entry_compiled) { return; }
  stats_data->curr.excluded_call_reg++;

}

static void gum_afl_stalker_stats_increment_ret_slow_path(
    GumStalkerObserver *observer) {

  UNUSED_PARAMETER(observer);

  if (!entry_compiled) { return; }
  stats_data->curr.ret_slow_path++;

}

static void gum_afl_stalker_stats_increment_ret(GumStalkerObserver *observer) {

  UNUSED_PARAMETER(observer);

  if (!entry_compiled) { return; }
  stats_data->curr.ret++;

}

static void gum_afl_stalker_stats_increment_post_call_invoke(
    GumStalkerObserver *observer) {

  UNUSED_PARAMETER(observer);

  if (!entry_compiled) { return; }
  stats_data->curr.post_call_invoke++;

}

static void gum_afl_stalker_stats_increment_excluded_call_imm(
    GumStalkerObserver *observer) {

  UNUSED_PARAMETER(observer);

  if (!entry_compiled) { return; }
  stats_data->curr.excluded_call_imm++;

}

static void gum_afl_stalker_stats_increment_jmp_imm(
    GumStalkerObserver *observer) {

  UNUSED_PARAMETER(observer);

  if (!entry_compiled) { return; }
  stats_data->curr.jmp_imm++;

}

static void gum_afl_stalker_stats_increment_jmp_reg(
    GumStalkerObserver *observer) {

  UNUSED_PARAMETER(observer);

  if (!entry_compiled) { return; }
  stats_data->curr.jmp_reg++;

}

static void gum_afl_stalker_stats_increment_jmp_mem(
    GumStalkerObserver *observer) {

  UNUSED_PARAMETER(observer);

  if (!entry_compiled) { return; }
  stats_data->curr.jmp_mem++;

}

static void gum_afl_stalker_stats_increment_jmp_cond_imm(
    GumStalkerObserver *observer) {

  UNUSED_PARAMETER(observer);

  if (!entry_compiled) { return; }
  stats_data->curr.jmp_cond_imm++;

}

static void gum_afl_stalker_stats_increment_jmp_cond_mem(
    GumStalkerObserver *observer) {

  UNUSED_PARAMETER(observer);

  if (!entry_compiled) { return; }
  stats_data->curr.jmp_cond_mem++;

}

static void gum_afl_stalker_stats_increment_jmp_cond_reg(
    GumStalkerObserver *observer) {

  UNUSED_PARAMETER(observer);

  if (!entry_compiled) { return; }
  stats_data->curr.jmp_cond_reg++;

}

static void gum_afl_stalker_stats_increment_jmp_cond_jcxz(
    GumStalkerObserver *observer) {

  UNUSED_PARAMETER(observer);

  if (!entry_compiled) { return; }
  stats_data->curr.jmp_cond_jcxz++;

}

static void gum_afl_stalker_stats_increment_jmp_cond_cc(
    GumStalkerObserver *observer) {

  UNUSED_PARAMETER(observer);

  if (!entry_compiled) { return; }
  stats_data->curr.jmp_cond_cc++;

}

static void gum_afl_stalker_stats_increment_jmp_cond_cbz(
    GumStalkerObserver *observer) {

  UNUSED_PARAMETER(observer);

  if (!entry_compiled) { return; }
  stats_data->curr.jmp_cond_cbz++;

}

static void gum_afl_stalker_stats_increment_jmp_cond_cbnz(
    GumStalkerObserver *observer) {

  UNUSED_PARAMETER(observer);

  if (!entry_compiled) { return; }
  stats_data->curr.jmp_cond_cbnz++;

}

static void gum_afl_stalker_stats_increment_jmp_cond_tbz(
    GumStalkerObserver *observer) {

  UNUSED_PARAMETER(observer);

  if (!entry_compiled) { return; }
  stats_data->curr.jmp_cond_tbz++;

}

static void gum_afl_stalker_stats_increment_jmp_cond_tbnz(
    GumStalkerObserver *observer) {

  UNUSED_PARAMETER(observer);

  if (!entry_compiled) { return; }
  stats_data->curr.jmp_cond_tbnz++;

}

static void gum_afl_stalker_stats_increment_jmp_continuation(
    GumStalkerObserver *observer) {

  UNUSED_PARAMETER(observer);

  if (!entry_compiled) { return; }
  stats_data->curr.jmp_continuation++;

}

static void stats_observer_init(GumStalkerObserver *observer) {

  GumStalkerObserverInterface *iface = GUM_STALKER_OBSERVER_GET_IFACE(observer);
  iface->increment_total = gum_afl_stalker_stats_increment_total;
  iface->increment_call_imm = gum_afl_stalker_stats_increment_call_imm;
  iface->increment_call_reg = gum_afl_stalker_stats_increment_call_reg;
  iface->increment_call_mem = gum_afl_stalker_stats_increment_call_mem;
  iface->increment_excluded_call_reg =
      gum_afl_stalker_stats_increment_excluded_call_reg;
  iface->increment_ret_slow_path =
      gum_afl_stalker_stats_increment_ret_slow_path;
  iface->increment_ret = gum_afl_stalker_stats_increment_ret;
  iface->increment_post_call_invoke =
      gum_afl_stalker_stats_increment_post_call_invoke;
  iface->increment_excluded_call_imm =
      gum_afl_stalker_stats_increment_excluded_call_imm;
  iface->increment_jmp_imm = gum_afl_stalker_stats_increment_jmp_imm;
  iface->increment_jmp_reg = gum_afl_stalker_stats_increment_jmp_reg;
  iface->increment_jmp_mem = gum_afl_stalker_stats_increment_jmp_mem;
  iface->increment_jmp_cond_imm = gum_afl_stalker_stats_increment_jmp_cond_imm;
  iface->increment_jmp_cond_mem = gum_afl_stalker_stats_increment_jmp_cond_mem;
  iface->increment_jmp_cond_reg = gum_afl_stalker_stats_increment_jmp_cond_reg;
  iface->increment_jmp_cond_jcxz =
      gum_afl_stalker_stats_increment_jmp_cond_jcxz;
  iface->increment_jmp_cond_cc = gum_afl_stalker_stats_increment_jmp_cond_cc;
  iface->increment_jmp_cond_cbz = gum_afl_stalker_stats_increment_jmp_cond_cbz;
  iface->increment_jmp_cond_cbnz =
      gum_afl_stalker_stats_increment_jmp_cond_cbnz;
  iface->increment_jmp_cond_tbz = gum_afl_stalker_stats_increment_jmp_cond_tbz;
  iface->increment_jmp_cond_tbnz =
      gum_afl_stalker_stats_increment_jmp_cond_tbnz;
  iface->increment_jmp_continuation =
      gum_afl_stalker_stats_increment_jmp_continuation;

}

void stats_config(void) {

  stats_filename = getenv("AFL_FRIDA_STATS_FILE");
  stats_interval = util_read_num("AFL_FRIDA_STATS_INTERVAL", 10);

}

void stats_init(void) {

  FOKF(cBLU "Stats" cRST " - " cGRN "file:" cYEL " [%s]",
       stats_filename == NULL ? " " : stats_filename);
  FOKF(cBLU "Stats" cRST " - " cGRN "interval:" cYEL " [%" G_GINT64_MODIFIER
            "u]",
       stats_interval);

  if (getenv("AFL_FRIDA_STATS_INTERVAL") != NULL &&
      getenv("AFL_FRIDA_STATS_FILE") == NULL) {

    FFATAL(
        "AFL_FRIDA_STATS_FILE must be specified if "
        "AFL_FRIDA_STATS_INTERVAL is");

  }

  stats_interval_us = stats_interval * MICRO_TO_SEC;

  if (stats_filename == NULL) { return; }

  char *path = g_canonicalize_filename(stats_filename, g_get_current_dir());

  FOKF(cBLU "Stats" cRST " - " cGRN "path:" cYEL " [%s]",
       path == NULL ? " " : path);

  stats_fd = open(path, O_RDWR | O_CREAT | O_TRUNC,
                  S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP);

  if (stats_fd < 0) { FFATAL("Failed to open stats file '%s'", path); }

  g_free(path);

  GumStalkerObserver *observer = stalker_get_observer();
  stats_observer_init(observer);

  stats_data = shm_create(sizeof(stats_data_t));

  starts_arch_init();

}

void stats_print(char *format, ...) {

  char buffer[4096] = {0};
  int  len;

  va_list ap;
  va_start(ap, format);

  if (vsnprintf(buffer, sizeof(buffer) - 1, format, ap) < 0) { return; }

  len = strnlen(buffer, sizeof(buffer));
  IGNORED_RETURN(write(stats_fd, buffer, len));
  va_end(ap);

}

void stats_on_fork(void) {

  stats_write();

}

void stats_collect(const cs_insn *instr, gboolean begin) {

  if (!entry_compiled) { return; }
  if (stats_filename == NULL) { return; }
  stats_collect_arch(instr, begin);

}

