/**
 *
 * Agora Real Time Engagement
 * Created by Wei Hu in 2021-09.
 * Copyright (c) 2022 Agora IO. All rights reserved.
 *
 */
#include "rte_runtime/binding/nodejs/src/common/common.h"

#include <stdarg.h>
#include <stdio.h>

#include "js_native_api.h"
#include "js_native_api_types.h"
#include "lib/alloc.h"
#include "lib/string.h"
#include "macro/mark.h"
#include "node_api.h"

#define ZF_LOG_TAG "Node"
#include "zf_log.h"

napi_value UNDEFINED(napi_env env) {
  assert(env);

  napi_value js_undefined_value = NULL;
  napi_status status = napi_get_undefined(env, &js_undefined_value);
  ASSERT_IF_NAPI_FAIL(status == napi_ok,
                      "Failed to get type JS undefined value: %d", status);

  return js_undefined_value;
}

bool is_undefined(napi_env env, napi_value val) {
  assert(env);

  napi_valuetype valuetype = 0;
  napi_status status = napi_typeof(env, val, &valuetype);
  ASSERT_IF_NAPI_FAIL(status == napi_ok,
                      "Failed to get type of JS instance: %d", status);

  return (valuetype == napi_undefined) ? true : false;
}

bool is_string(napi_env env, napi_value val) {
  assert(env);

  napi_valuetype valuetype = 0;
  napi_status status = napi_typeof(env, val, &valuetype);
  ASSERT_IF_NAPI_FAIL(status == napi_ok,
                      "Failed to get type of JS instance: %d", status);

  return (valuetype == napi_string) ? true : false;
}

bool is_object(napi_env env, napi_value val) {
  assert(env);

  napi_valuetype valuetype = 0;
  napi_status status = napi_typeof(env, val, &valuetype);
  ASSERT_IF_NAPI_FAIL(status == napi_ok,
                      "Failed to get type of JS instance: %d", status);

  return (valuetype == napi_object) ? true : false;
}

bool is_array(napi_env env, napi_value val) {
  assert(env && val);

  bool is_array = false;
  napi_status status = napi_is_array(env, val, &is_array);
  ASSERT_IF_NAPI_FAIL(
      status == napi_ok,
      "Failed to determine if a JS instance is array or not: %d", status);

  return is_array;
}

bool is_buffer(napi_env env, napi_value val) {
  assert(env && val);

  bool is_array_buffer = false;
  napi_status status = napi_is_buffer(env, val, &is_array_buffer);
  ASSERT_IF_NAPI_FAIL(
      status == napi_ok,
      "Failed to determine if a JS instance is buffer or not: %d", status);

  return is_array_buffer;
}

// Plug 'func' of JS callback type into the 'exports' table of the current JS
// module.
void rte_nodejs_export_func(napi_env env, napi_value exports,
                            const char* func_name, napi_callback func) {
  assert(func_name && strlen(func_name));

  napi_value fn = NULL;
  napi_status status =
      napi_create_function(env, func_name, NAPI_AUTO_LENGTH, func, NULL, &fn);
  ASSERT_IF_NAPI_FAIL(status == napi_ok, "Failed to create JS function: %d",
                      status);

  status = napi_set_named_property(env, exports, func_name, fn);
  ASSERT_IF_NAPI_FAIL(
      status == napi_ok,
      "Failed to add newly created JS function to 'exports': %d", status);
}

bool rte_nodejs_get_args(napi_env env, napi_callback_info info,
                         napi_value* args, size_t argc) {
  assert(env);
  assert(info);
  assert(args);

  size_t actual_argc = argc;
  napi_status status =
      napi_get_cb_info(env, info, &actual_argc, args, NULL, NULL);
  ASSERT_IF_NAPI_FAIL(status == napi_ok,
                      "Failed to get JS function arguments: %d", status);

  if (actual_argc != argc) {
    rte_string_t err;
    rte_string_init_with_value(
        &err, "Expected %zu arguments, got %zu arguments", argc, actual_argc);

    ZF_LOGE("%s", rte_string_c_str(&err));

    status = napi_throw_error(env, "EINVAL", rte_string_c_str(&err));
    ASSERT_IF_NAPI_FAIL(status == napi_ok, "Failed to throw JS exception: %d",
                        status);

    rte_string_deinit(&err);
    return false;
  }

  return true;
}

bool rte_nodejs_get_str(napi_env env, napi_value val, rte_string_t* str) {
  assert(env && val && str);

  napi_status status = napi_ok;

  if (is_string(env, val) == false) {
    status = napi_throw_error(env, "EINVAL", "Expected a string");
    ASSERT_IF_NAPI_FAIL(status == napi_ok, "Failed to throw JS exception: %d",
                        status);
    return false;
  }

  size_t str_len = 0;
  status = napi_get_value_string_utf8(env, val, NULL, 0, &str_len);
  ASSERT_IF_NAPI_FAIL(status == napi_ok, "Failed to get JS string length: %d",
                      status);

  rte_string_reserve(str, str_len + 1);

  status =
      napi_get_value_string_utf8(env, val, str->buf, str_len + 1, &str_len);
  ASSERT_IF_NAPI_FAIL(status == napi_ok, "Failed to get JS string: %d", status);

  str->buf[str_len] = '\0';

  return true;
}

static bool rte_nodejs_get_loc_field(napi_env env, napi_value val,
                                     const char* field_name,
                                     rte_string_t* str) {
  assert(env && val && field_name && str);

  bool result = true;
  if (napi_has_named_property(env, val, field_name, &result) != napi_ok ||
      result == false) {
    return false;
  }

  napi_value uri = NULL;
  napi_status status = napi_get_named_property(env, val, field_name, &uri);
  ASSERT_IF_NAPI_FAIL(status == napi_ok,
                      "Failed to get property %s of JS instance: %d",
                      field_name, status);

  rte_string_init(str);
  if (!rte_nodejs_get_str(env, uri, str)) {
    return false;
  }

  return true;
}

bool rte_nodejs_get_loc(napi_env env, napi_value val, rte_loc_t* loc) {
  assert(env && val && loc);

  napi_status status = napi_ok;

  if (!is_object(env, val)) {
    status = napi_throw_error(env, "EINVAL", "Expected an object.");
    ASSERT_IF_NAPI_FAIL(status == napi_ok, "Failed to throw JS exception: %d",
                        status);
    return false;
  }

  rte_nodejs_get_loc_field(env, val, "uri", &loc->uri);
  rte_nodejs_get_loc_field(env, val, "graph_id", &loc->graph_id);
  rte_nodejs_get_loc_field(env, val, "extension_group_name",
                           &loc->extension_group_name);
  rte_nodejs_get_loc_field(env, val, "extension_name", &loc->extension_name);

  return true;
}

void* rte_nodejs_get_buffer(napi_env env, napi_value val, size_t* len_in_byte) {
  assert(env && val);

  napi_status status = napi_ok;

  if (is_object(env, val) == false) {
    status = napi_throw_error(env, "EINVAL", "Expected a object");
    ASSERT_IF_NAPI_FAIL(status == napi_ok, "Failed to throw JS exception: %d",
                        status);
    return NULL;
  }

  if (is_buffer(env, val) == false) {
    status = napi_throw_error(env, "EINVAL", "Expected an ArrayBuffer");
    ASSERT_IF_NAPI_FAIL(status == napi_ok, "Failed to throw JS exception: %d",
                        status);
    return NULL;
  }

  void* result = NULL;
  size_t len_in_byte_ = 0;
  status = napi_get_buffer_info(env, val, &result, &len_in_byte_);
  ASSERT_IF_NAPI_FAIL(status == napi_ok, "Failed to get JS buffer info: %d",
                      status);

  if (len_in_byte) {
    *len_in_byte = len_in_byte_;
  }
  return result;
}

bool rte_nodejs_get_arr_len(napi_env env, napi_value val, uint32_t* arr_len) {
  assert(env && val && arr_len);

  napi_status status = napi_ok;

  if (is_object(env, val) == false) {
    status = napi_throw_error((env), "EINVAL", "Expected a object");
    ASSERT_IF_NAPI_FAIL(status == napi_ok, "Failed to throw JS exception: %d",
                        status);
    return false;
  }

  if (is_array(env, val) == false) {
    status = napi_throw_error((env), "EINVAL", "Expected an Array");
    ASSERT_IF_NAPI_FAIL(status == napi_ok, "Failed to throw JS exception: %d",
                        status);
    return false;
  }

  status = napi_get_array_length(env, val, arr_len);
  ASSERT_IF_NAPI_FAIL(status == napi_ok, "Failed to get JS array length: %d",
                      status);

  return true;
}

bool rte_nodejs_get_arr(napi_env env, napi_value val, napi_value* arr,
                        uint32_t arr_len) {
  assert(env && val);

  napi_status status = napi_ok;

  if (is_object(env, val) == false) {
    status = napi_throw_error((env), "EINVAL", "Expected a object");
    ASSERT_IF_NAPI_FAIL(status == napi_ok, "Failed to throw JS exception: %d",
                        status);
    return false;
  }

  if (is_array(env, val) == false) {
    status = napi_throw_error((env), "EINVAL", "Expected an Array");
    ASSERT_IF_NAPI_FAIL(status == napi_ok, "Failed to throw JS exception: %d",
                        status);
    return false;
  }

  for (size_t i = 0; i < arr_len; ++i) {
    status = napi_get_element(env, val, i, &arr[i]);
    ASSERT_IF_NAPI_FAIL(status == napi_ok,
                        "Failed to get JS array element at index %zu: %d", i,
                        status);
  }

  return true;
}

void rte_nodejs_report_and_clear_error_(napi_env env, napi_status orig_status,
                                        const char* func, int line) {
  assert(func);

  ZF_LOGE("(%s:%d) Failed to invoke napi function, status: %d", func, line,
          orig_status);

  const napi_extended_error_info* error_info = NULL;
  napi_status status = napi_get_last_error_info((env), &error_info);
  ASSERT_IF_NAPI_FAIL(status == napi_ok,
                      "Failed to get napi last error info: %d", status);

  const char* err_message = error_info->error_message;

  ZF_LOGE("napi error message: %s", err_message);

  // Check if there is any pending JS exception.
  bool pending = false;
  status = napi_is_exception_pending(env, &pending);
  ASSERT_IF_NAPI_FAIL(status == napi_ok,
                      "Failed to check if there is any pending JS exceptions "
                      "in the JS world: %d",
                      status);
  if (pending) {
    napi_value ex = NULL;
    status = napi_get_and_clear_last_exception(env, &ex);
    ASSERT_IF_NAPI_FAIL(status == napi_ok || ex != NULL,
                        "Failed to get latest JS exception: %d", status);

    napi_value ex_str = NULL;
    status = napi_coerce_to_string(env, ex, &ex_str);
    ASSERT_IF_NAPI_FAIL(status == napi_ok && ex_str != NULL,
                        "Failed to coerce JS exception string: %d", status);

    size_t str_size = 0;
    status = napi_get_value_string_utf8(env, ex_str, NULL, str_size, &str_size);
    ASSERT_IF_NAPI_FAIL(status == napi_ok,
                        "Failed to get the JS exception string length: %d",
                        status);

    char* buf = rte_malloc(str_size + 1);
    assert(buf);

    status = napi_get_value_string_utf8(env, ex_str, buf, str_size + 1, NULL);
    ASSERT_IF_NAPI_FAIL(status == napi_ok,
                        "Failed to get JS exception string: %d", status);

    buf[str_size] = '\0';
    ZF_LOGE("Exception: %s", buf);

    rte_free(buf);

    // Trigger an 'uncaughtException' in JavaScript. Useful if an async callback
    // throws an exception with no way to recover.
    status = napi_fatal_exception(env, ex);
    ASSERT_IF_NAPI_FAIL(status == napi_ok,
                        "Failed to throw JS fatal exception: %d", status);
  } else {
    ZF_LOGV("No pending exceptions when napi API failed.");

    // Encountering napi error but can not get any exceptions. It means JS
    // runtime went wrong (one possible reason is that the JS runtime is
    // closing), so we don't throw exceptions into JS runtime here.
  }
}

/**
 * @brief Create a JavaScript object by using a 'constructor' function in the
 * JavaScript world, and binding the newly created JavaScript object to a native
 * object in 'bridge_obj'.
 */
napi_value rte_nodejs_create_new_js_object_and_wrap(napi_env env,
                                                    napi_ref js_constructor_ref,
                                                    void* bridge_obj,
                                                    napi_finalize finalizer,
                                                    napi_ref* bridge_weak_ref) {
  assert(env);
  assert(js_constructor_ref);
  assert(bridge_obj);

  napi_value js_instance = NULL;

  // Get the JavaScript constructor function corresponding to the
  // 'constructor_ref'.
  napi_value js_constructor = NULL;
  napi_status status =
      napi_get_reference_value(env, js_constructor_ref, &js_constructor);
  GOTO_LABEL_IF_NAPI_FAIL(done, status == napi_ok && js_constructor != NULL,
                          "Failed to get constructor ref, status: %d", status);

  // Create a JS instance.
  status = napi_new_instance(env, js_constructor, 0, NULL, &js_instance);
  GOTO_LABEL_IF_NAPI_FAIL(done, status == napi_ok,
                          "Failed to create instance: %d", status);

  // Wrap the native 'bridge_obj' to the newly created JS instance.
  status =
      napi_wrap(env, js_instance, bridge_obj, finalizer, NULL, bridge_weak_ref);
  GOTO_LABEL_IF_NAPI_FAIL(done, status == napi_ok,
                          "Failed to bind JS instance & bridge: %d", status);

done:
  return js_instance;
}

napi_value rte_nodejs_get_property(napi_env env, napi_value self,
                                   const char* name) {
  assert(env);
  assert(name);

  napi_value key = NULL;
  UNUSED napi_status status =
      napi_create_string_utf8(env, name, strlen(name), &key);
  ASSERT_IF_NAPI_FAIL(status == napi_ok, "Failed to create JS string: %d",
                      status);

  napi_value value = NULL;
  status = napi_get_property(env, self, key, &value);
  ASSERT_IF_NAPI_FAIL(status == napi_ok, "Failed to get JS property: %d",
                      status);

  return value;
}
