#include "frida-gumjs.h"

#include "js.h"
#include "util.h"

gboolean                  js_done = FALSE;
js_api_stalker_callback_t js_user_callback = NULL;
js_main_hook_t            js_main_hook = NULL;

static char               *js_script = NULL;
static gchar              *filename = "afl.js";
static gchar              *contents;
static GumScriptBackend   *backend;
static GCancellable       *cancellable = NULL;
static GError             *error = NULL;
static GumScript          *script;
static GumScriptScheduler *scheduler;
static GMainContext       *context;
static GMainLoop          *main_loop;

static void js_msg(const gchar *message, GBytes *data, gpointer user_data) {

  UNUSED_PARAMETER(data);
  UNUSED_PARAMETER(user_data);
  FOKF("%s", message);

}

void js_config(void) {

  js_script = getenv("AFL_FRIDA_JS_SCRIPT");

}

static gchar *js_get_script() {

  gsize length;
  if (js_script != NULL) { filename = js_script; }

  filename = g_canonicalize_filename(filename, g_get_current_dir());

  if (!g_file_get_contents(filename, &contents, &length, NULL)) {

    if (js_script == NULL) {

      return NULL;

    } else {

      FFATAL("Could not load script file: %s", filename);

    }

  } else {

    FOKF(cBLU "Javascript" cRST " - " cGRN "script:" cYEL " [%s]",
         filename == NULL ? " " : filename);
    FOKF(cBLU "Javascript" cRST " - " cGRN "size: " cYEL "%" G_GSIZE_MODIFIER
              "d bytes",
         length);

    gchar *source = g_malloc0(api_js_len + length + 1);
    memcpy(source, api_js, api_js_len);
    memcpy(&source[api_js_len], contents, length);

    return source;

  }

}

static void js_print_script(gchar *source) {

  gchar **split = g_strsplit(source, "\n", 0);

  for (size_t i = 0; split[i] != NULL; i++) {

    FVERBOSE("%3" G_GSIZE_MODIFIER "d. %s", i + 1, split[i]);

  }

  g_strfreev(split);

}

static void load_cb(GObject *source_object, GAsyncResult *result,
                    gpointer user_data) {

  UNUSED_PARAMETER(source_object);
  UNUSED_PARAMETER(user_data);
  gum_script_load_finish(script, result);
  if (error != NULL) { FFATAL("Failed to load script - %s", error->message); }

}

static void create_cb(GObject *source_object, GAsyncResult *result,
                      gpointer user_data) {

  UNUSED_PARAMETER(source_object);
  UNUSED_PARAMETER(user_data);
  script = gum_script_backend_create_finish(backend, result, &error);
  if (error != NULL) { FFATAL("Failed to create script: %s", error->message); }

  gum_script_set_message_handler(script, js_msg, NULL, NULL);

  gum_script_load(script, cancellable, load_cb, NULL);

}

void js_start(void) {

  gchar *source = js_get_script();
  if (source == NULL) { return; }
  js_print_script(source);

  scheduler = gum_script_backend_get_scheduler();
  gum_script_scheduler_disable_background_thread(scheduler);

  backend = gum_script_backend_obtain_qjs();

  context = gum_script_scheduler_get_js_context(scheduler);
  main_loop = g_main_loop_new(context, true);
  g_main_context_push_thread_default(context);

  gum_script_backend_create(backend, "example", source, NULL, cancellable,
                            create_cb, &error);

  while (g_main_context_pending(context))
    g_main_context_iteration(context, FALSE);

  if (!js_done) { FFATAL("Script didn't call Afl.done()"); }

}

gboolean js_stalker_callback(const cs_insn *insn, gboolean begin,
                             gboolean excluded, GumStalkerOutput *output) {

  if (js_user_callback == NULL) { return TRUE; }
  return js_user_callback(insn, begin, excluded, output);

}

