#include <sys/mman.h>

#include "frida-gumjs.h"

#include "ranges.h"
#include "shm.h"
#include "stats.h"
#include "util.h"

#define MICRO_TO_SEC 1000000

#if defined(__x86_64__) || defined(__i386__)

typedef struct {

  guint64 num_blocks;
  guint64 num_instructions;

  guint64 num_eob;

  guint64 num_call_imm;
  guint64 num_call_imm_excluded;
  guint64 num_call_reg;
  guint64 num_call_mem;

  guint64 num_jmp_imm;
  guint64 num_jmp_reg;
  guint64 num_jmp_mem;

  guint64 num_jmp_cond_imm;
  guint64 num_jmp_cond_reg;
  guint64 num_jmp_cond_mem;

  guint64 num_jmp_cond_jcxz;

  guint64 num_ret;

  guint64 num_rip_relative;

  guint64 num_rip_relative_type[X86_INS_ENDING];
  char    name_rip_relative_type[X86_INS_ENDING][CS_MNEMONIC_SIZE];

} stats_data_arch_t;

static stats_data_arch_t *stats_data_arch = NULL;

void starts_arch_init(void) {

  stats_data_arch = shm_create(sizeof(stats_data_arch_t));

}

static void stats_write_arch_stat(char *label, guint64 value, guint64 total) {

  stats_print("%-30s ", label);
  stats_print("%10" G_GINT64_MODIFIER "u ", value);
  if (total == 0) {

    stats_print("(--.--%%), ");

  } else {

    stats_print("(%5.2f%%) ", ((float)value * 100) / total);

  }

  stats_print("\n");

}

static void stats_write_arch_stat_delta(char *label, guint64 prev_value,
                                        guint64 curr_value, guint elapsed,
                                        guint64 prev_total,
                                        guint64 curr_total) {

  guint64 delta = curr_value - prev_value;
  guint64 delta_total = curr_total - prev_total;
  guint64 per_sec = delta / elapsed;

  stats_print("%-30s ", label);

  stats_print("%10" G_GINT64_MODIFIER "u ", curr_value);
  if (curr_total == 0) {

    stats_print("(--.--%%), ");

  } else {

    stats_print("(%5.2f%%) ", ((float)curr_value * 100) / curr_total);

  }

  stats_print("%10" G_GINT64_MODIFIER "u ", delta);
  if (delta_total == 0) {

    stats_print("(--.--%%), ");

  } else {

    stats_print("(%5.2f%%) ", ((float)delta * 100) / delta_total);

  }

  stats_print("[%10" G_GINT64_MODIFIER "u/s]", per_sec);
  stats_print("\n");

}

void stats_write_arch(stats_data_t *data) {

  guint elapsed =
      (data->curr.stats_time - data->prev.stats_time) / MICRO_TO_SEC;
  stats_print("%-30s %10s %19s\n", "Transitions", "cumulative", "delta");
  stats_print("%-30s %10s %19s\n", "-----------", "----------", "-----");
  stats_print(
      "%-30s %10" G_GINT64_MODIFIER "u %-8s %10" G_GINT64_MODIFIER "u\n",
      "total", data->curr.total, "", data->curr.total - data->prev.total);
  stats_write_arch_stat_delta("call_imm", data->prev.call_imm,
                              data->curr.call_imm, elapsed, data->prev.total,
                              data->curr.total);
  stats_write_arch_stat_delta("call_reg", data->prev.call_reg,
                              data->curr.call_reg, elapsed, data->prev.total,
                              data->curr.total);
  stats_write_arch_stat_delta("call_mem", data->prev.call_mem,
                              data->curr.call_mem, elapsed, data->prev.total,
                              data->curr.total);
  stats_write_arch_stat_delta("ret_slow_path", data->prev.ret_slow_path,
                              data->curr.ret_slow_path, elapsed,
                              data->prev.total, data->curr.total);
  stats_write_arch_stat_delta("post_call_invoke", data->prev.post_call_invoke,
                              data->curr.post_call_invoke, elapsed,
                              data->prev.total, data->curr.total);
  stats_write_arch_stat_delta("excluded_call_imm", data->prev.excluded_call_imm,
                              data->curr.excluded_call_imm, elapsed,
                              data->prev.total, data->curr.total);
  stats_write_arch_stat_delta("jmp_imm", data->prev.jmp_imm, data->curr.jmp_imm,
                              elapsed, data->prev.total, data->curr.total);
  stats_write_arch_stat_delta("jmp_reg", data->prev.jmp_reg, data->curr.jmp_reg,
                              elapsed, data->prev.total, data->curr.total);
  stats_write_arch_stat_delta("jmp_mem", data->prev.jmp_mem, data->curr.jmp_mem,
                              elapsed, data->prev.total, data->curr.total);
  stats_write_arch_stat_delta("jmp_cond_imm", data->prev.jmp_cond_imm,
                              data->curr.jmp_cond_imm, elapsed,
                              data->prev.total, data->curr.total);
  stats_write_arch_stat_delta("jmp_cond_mem", data->prev.jmp_cond_mem,
                              data->curr.jmp_cond_mem, elapsed,
                              data->prev.total, data->curr.total);
  stats_write_arch_stat_delta("jmp_cond_reg", data->prev.jmp_cond_reg,
                              data->curr.jmp_cond_reg, elapsed,
                              data->prev.total, data->curr.total);
  stats_write_arch_stat_delta("jmp_cond_jcxz", data->prev.jmp_cond_jcxz,
                              data->curr.jmp_cond_jcxz, elapsed,
                              data->prev.total, data->curr.total);
  stats_write_arch_stat_delta("jmp_continuation", data->prev.jmp_continuation,
                              data->curr.jmp_continuation, elapsed,
                              data->prev.total, data->curr.total);
  stats_print("\n");
  stats_print("\n");

  stats_print("Instrumentation\n");
  stats_print("---------------\n");
  stats_print("%-30s %10" G_GINT64_MODIFIER "u\n", "Instructions",
              stats_data_arch->num_instructions);
  stats_print("%-30s %10" G_GINT64_MODIFIER "u\n", "Blocks",
              stats_data_arch->num_blocks);

  if (stats_data_arch->num_blocks != 0) {

    stats_print(
        "%-30s %10" G_GINT64_MODIFIER "u\n", "Avg Instructions / Block ",
        stats_data_arch->num_instructions / stats_data_arch->num_blocks);

  }

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

  guint64 num_instructions = stats_data_arch->num_instructions;

  stats_print("EOB Instructions\n");
  stats_print("----------------\n");
  stats_write_arch_stat("Total", stats_data_arch->num_eob, num_instructions);
  stats_write_arch_stat("Call Immediates", stats_data_arch->num_call_imm,
                        num_instructions);
  stats_write_arch_stat("Call Immediates Excluded",
                        stats_data_arch->num_call_imm_excluded,
                        num_instructions);
  stats_write_arch_stat("Call Register", stats_data_arch->num_call_reg,
                        num_instructions);
  stats_write_arch_stat("Call Memory", stats_data_arch->num_call_mem,
                        num_instructions);
  stats_write_arch_stat("Jump Immediates", stats_data_arch->num_jmp_imm,
                        num_instructions);
  stats_write_arch_stat("Jump Register", stats_data_arch->num_jmp_reg,
                        num_instructions);
  stats_write_arch_stat("Jump Memory", stats_data_arch->num_jmp_mem,
                        num_instructions);
  stats_write_arch_stat("Conditional Jump Immediates",
                        stats_data_arch->num_jmp_cond_imm, num_instructions);
  stats_write_arch_stat("Conditional Jump CX Immediate",
                        stats_data_arch->num_jmp_cond_jcxz, num_instructions);
  stats_write_arch_stat("Conditional Jump Register",
                        stats_data_arch->num_jmp_cond_reg, num_instructions);
  stats_write_arch_stat("Conditional Jump Memory",
                        stats_data_arch->num_jmp_cond_mem, num_instructions);
  stats_write_arch_stat("Returns", stats_data_arch->num_ret, num_instructions);
  stats_print("\n");
  stats_print("\n");

  stats_print("Relocated Instructions\n");
  stats_print("----------------------\n");
  stats_write_arch_stat("Total", stats_data_arch->num_rip_relative,
                        num_instructions);

  for (size_t i = 0; i < X86_INS_ENDING; i++) {

    if (stats_data_arch->num_rip_relative_type[i] != 0) {

      stats_write_arch_stat(stats_data_arch->name_rip_relative_type[i],
                            stats_data_arch->num_rip_relative_type[i],
                            stats_data_arch->num_rip_relative);

    }

  }

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

}

static x86_op_type stats_get_operand_type(const cs_insn *instr) {

  cs_x86    *x86 = &instr->detail->x86;
  cs_x86_op *operand;

  if (x86->op_count != 1) {

    FFATAL("Unexpected operand count (%d): %s %s\n", x86->op_count,
           instr->mnemonic, instr->op_str);

  }

  operand = &x86->operands[0];

  return operand->type;

}

static void stats_collect_call_imm_excluded_arch(const cs_insn *instr) {

  cs_x86    *x86 = &instr->detail->x86;
  cs_x86_op *operand = &x86->operands[0];

  if (range_is_excluded(GUM_ADDRESS(operand->imm))) {

    stats_data_arch->num_call_imm_excluded++;

  }

}

static void stats_collect_call_arch(const cs_insn *instr) {

  x86_op_type type = stats_get_operand_type(instr);
  switch (type) {

    case X86_OP_IMM:
      stats_data_arch->num_call_imm++;
      stats_collect_call_imm_excluded_arch(instr);
      break;
    case X86_OP_REG:
      stats_data_arch->num_call_reg++;
      break;
    case X86_OP_MEM:
      stats_data_arch->num_call_mem++;
      break;
    default:
      FFATAL("Invalid operand type: %s %s\n", instr->mnemonic, instr->op_str);

  }

}

static void stats_collect_jump_arch(const cs_insn *instr) {

  x86_op_type type = stats_get_operand_type(instr);
  switch (type) {

    case X86_OP_IMM:
      stats_data_arch->num_jmp_imm++;
      break;
    case X86_OP_REG:
      stats_data_arch->num_jmp_reg++;
      break;
    case X86_OP_MEM:
      stats_data_arch->num_jmp_mem++;
      break;
    default:
      FFATAL("Invalid operand type: %s %s\n", instr->mnemonic, instr->op_str);

  }

}

static void stats_collect_jump_cond_arch(const cs_insn *instr) {

  x86_op_type type = stats_get_operand_type(instr);
  switch (type) {

    case X86_OP_IMM:
      stats_data_arch->num_jmp_cond_imm++;
      break;
    case X86_OP_REG:
      stats_data_arch->num_jmp_cond_reg++;
      break;
    case X86_OP_MEM:
      stats_data_arch->num_jmp_cond_mem++;
      break;
    default:
      FFATAL("Invalid operand type: %s %s\n", instr->mnemonic, instr->op_str);

  }

}

static void stats_collect_rip_relative_arch(const cs_insn *instr) {

  cs_x86 *x86 = &instr->detail->x86;
  guint   mod;
  guint   rm;

  if (x86->encoding.modrm_offset == 0) { return; }

  mod = (x86->modrm & 0xc0) >> 6;
  if (mod != 0) { return; }

  rm = (x86->modrm & 0x07) >> 0;
  if (rm != 5) { return; }

  stats_data_arch->num_rip_relative++;
  stats_data_arch->num_rip_relative_type[instr->id]++;
  memcpy(stats_data_arch->name_rip_relative_type[instr->id], instr->mnemonic,
         CS_MNEMONIC_SIZE);

}

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

  if (stats_data_arch == NULL) { return; }
  if (begin) { stats_data_arch->num_blocks++; }
  stats_data_arch->num_instructions++;

  switch (instr->id) {

    case X86_INS_CALL:
      stats_collect_call_arch(instr);
      stats_data_arch->num_eob++;
      break;
    case X86_INS_JMP:
      stats_collect_jump_arch(instr);
      stats_data_arch->num_eob++;
      break;
    case X86_INS_JA:
    case X86_INS_JAE:
    case X86_INS_JB:
    case X86_INS_JBE:
    case X86_INS_JE:
    case X86_INS_JG:
    case X86_INS_JGE:
    case X86_INS_JL:
    case X86_INS_JLE:
    case X86_INS_JNE:
    case X86_INS_JNO:
    case X86_INS_JNP:
    case X86_INS_JNS:
    case X86_INS_JO:
    case X86_INS_JP:
    case X86_INS_JS:
      stats_collect_jump_cond_arch(instr);
      stats_data_arch->num_eob++;
      break;
    case X86_INS_JECXZ:
    case X86_INS_JRCXZ:
      stats_data_arch->num_jmp_cond_jcxz++;
      stats_data_arch->num_eob++;
      break;
    case X86_INS_RET:
      stats_data_arch->num_ret++;
      stats_data_arch->num_eob++;
      break;
    default:
      stats_collect_rip_relative_arch(instr);
      break;

  }

}

#endif

