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

#include "jansson.h"
#include "js_native_api.h"
#include "lib/alloc.h"
#include "lib/atomic.h"
#include "lib/shared_ptr.h"
#include "lib/signature.h"
#include "lib/string.h"
#include "rte_runtime/binding/nodejs/src/common/common.h"
#include "rte_runtime/binding/nodejs/src/value/value.h"
#include "rte_runtime/common/loc.h"
#include "rte_runtime/msg/msg.h"
#include "rte_runtime/rte/rte.h"
#include "rte_runtime/sanitizer/thread_check.h"
#include "schema/value.h"

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

bool rte_nodejs_msg_check_integrity(rte_nodejs_msg_t* self, bool check_thread) {
  assert(self);

  if (rte_signature_get(&self->signature) != RTE_NODEJS_MSG_SIGNATURE) {
    return false;
  }

  if (check_thread &&
      !rte_sanitizer_thread_check_do_check(&self->thread_check)) {
    return false;
  }

  return true;
}

static napi_value rte_nodejs_msg_get_src_extension(napi_env env,
                                                   napi_callback_info info) {
  assert(env && info);

  const size_t argc = 1;
  napi_value args[argc];  // msg
  bool rc = rte_nodejs_get_args(env, info, args, argc);
  if (!rc) {
    return UNDEFINED(env);
  }

  rte_nodejs_msg_t* msg_bridge = NULL;
  napi_status status = napi_unwrap(env, args[0], (void**)&msg_bridge);
  ASSERT_IF_NAPI_FAIL(status == napi_ok && msg_bridge != NULL,
                      "Failed to get msg bridge: %d", status);
  assert(msg_bridge && rte_nodejs_msg_check_integrity(msg_bridge, true));

  rte_loc_t* src_loc = rte_msg_get_src_loc(msg_bridge->c_msg);
  assert(src_loc);

  napi_value src_extension_js = NULL;
  status = napi_create_string_utf8(
      env, rte_string_c_str(&src_loc->extension_name),
      rte_string_len(&src_loc->extension_name), &src_extension_js);
  RETURN_UNDEFINED_IF_NAPI_FAIL(status == napi_ok && src_extension_js != NULL,
                                "Failed to create JS string: %d", status);

  return src_extension_js;
}

static napi_value rte_nodejs_msg_set_dest(napi_env env,
                                          napi_callback_info info) {
  assert(env && info);

  const size_t argc = 2;
  napi_value args[argc];  // Msg, dest_loc
  bool rc = rte_nodejs_get_args(env, info, args, argc);
  if (!rc) {
    return UNDEFINED(env);
  }

  rte_nodejs_msg_t* msg_bridge = NULL;
  napi_status status = napi_unwrap(env, args[0], (void**)&msg_bridge);
  ASSERT_IF_NAPI_FAIL(status == napi_ok && msg_bridge != NULL,
                      "Failed to get msg bridge: %d", status);
  assert(msg_bridge && rte_nodejs_msg_check_integrity(msg_bridge, true));

  rte_loc_t dest_loc;
  rte_loc_init_empty(&dest_loc);
  if (!rte_nodejs_get_loc(env, args[1], &dest_loc)) {
    return UNDEFINED(env);
  }

  rte_msg_set_dest_to_loc(msg_bridge->c_msg, &dest_loc);

  return UNDEFINED(env);
}

static napi_value rte_nodejs_msg_set_property(napi_env env,
                                              napi_callback_info info) {
  assert(env && info);

  rte_string_t name;
  rte_string_init(&name);

  const size_t argc = 3;
  napi_value args[argc];  // Msg, name, value
  bool rc = rte_nodejs_get_args(env, info, args, argc);
  if (!rc) {
    goto done;
  }

  rte_nodejs_msg_t* msg_bridge = NULL;
  napi_status status = napi_unwrap(env, args[0], (void**)&msg_bridge);
  ASSERT_IF_NAPI_FAIL(status == napi_ok && msg_bridge != NULL,
                      "Failed to get msg bridge: %d", status);
  assert(msg_bridge && rte_nodejs_msg_check_integrity(msg_bridge, true));

  if (!rte_nodejs_get_str(env, args[1], &name)) {
    goto done;
  }

  rte_nodejs_value_t* value_bridge = NULL;
  status = napi_unwrap(env, args[2], (void**)&value_bridge);
  GOTO_LABEL_IF_NAPI_FAIL(done, status == napi_ok && value_bridge != NULL,
                          "Failed to get value bridge: %d", status);
  assert(value_bridge && rte_nodejs_value_check_integrity(value_bridge, true));

  rte_msg_set_property(
      msg_bridge->c_msg,
      rte_value_kv_create(rte_string_c_str(&name),
                          rte_value_clone(value_bridge->c_value)));

done:
  rte_string_deinit(&name);
  return UNDEFINED(env);
}

static napi_value rte_nodejs_msg_get_property(napi_env env,
                                              napi_callback_info info) {
  assert(env && info);

  rte_string_t name;
  rte_string_init(&name);

  napi_value result = UNDEFINED(env);

  const size_t argc = 2;
  napi_value args[argc];  // msg, name
  bool rc = rte_nodejs_get_args(env, info, args, argc);
  if (!rc) {
    goto done;
  }

  rte_nodejs_msg_t* msg_bridge = NULL;
  napi_status status = napi_unwrap(env, args[0], (void**)&msg_bridge);
  ASSERT_IF_NAPI_FAIL(status == napi_ok && msg_bridge != NULL,
                      "Failed to get msg bridge: %d", status);
  assert(msg_bridge && rte_nodejs_msg_check_integrity(msg_bridge, true));

  if (!rte_nodejs_get_str(env, args[1], &name)) {
    goto done;
  }

  rte_value_kv_t* kv =
      rte_msg_get_property(msg_bridge->c_msg, rte_string_c_str(&name));
  if (kv) {
    result =
        rte_nodejs_value_wrap(env, rte_value_clone(rte_value_kv_get_value(kv)));
  }

done:
  rte_string_deinit(&name);
  return result;
}

static napi_value rte_nodejs_msg_get_src_loc(napi_env env,
                                             napi_callback_info info) {
  assert(env && info);

  const size_t argc = 1;
  napi_value args[argc];  // msg
  bool rc = rte_nodejs_get_args(env, info, args, argc);
  if (!rc) {
    return UNDEFINED(env);
  }

  rte_nodejs_msg_t* msg_bridge = NULL;
  napi_status status = napi_unwrap(env, args[0], (void**)&msg_bridge);
  ASSERT_IF_NAPI_FAIL(status == napi_ok && msg_bridge != NULL,
                      "Failed to get msg bridge: %d", status);
  assert(msg_bridge && rte_nodejs_msg_check_integrity(msg_bridge, true));

  rte_loc_t* src_loc = rte_msg_get_src_loc(msg_bridge->c_msg);
  assert(src_loc);

  rte_string_t* src_str = rte_string_create();
  assert(src_str);

  rte_loc_to_json_string(src_loc, src_str);

  napi_value src_loc_js = NULL;
  status = napi_create_string_utf8(env, rte_string_c_str(src_str),
                                   rte_string_len(src_str), &src_loc_js);

  rte_string_destroy(src_str);

  RETURN_UNDEFINED_IF_NAPI_FAIL(status == napi_ok,
                                "Failed to create JS string: %d", status);

  return src_loc_js;
}

static napi_value rte_nodejs_msg_get_dest_locs(napi_env env,
                                               napi_callback_info info) {
  assert(env && info);

  const size_t argc = 1;
  napi_value args[argc];  // msg
  bool rc = rte_nodejs_get_args(env, info, args, argc);
  if (!rc) {
    return UNDEFINED(env);
  }

  rte_nodejs_msg_t* msg_bridge = NULL;
  napi_status status = napi_unwrap(env, args[0], (void**)&msg_bridge);
  ASSERT_IF_NAPI_FAIL(status == napi_ok && msg_bridge != NULL,
                      "Failed to get msg bridge: %d", status);
  assert(msg_bridge && rte_nodejs_msg_check_integrity(msg_bridge, true));

  rte_list_t* dest_locs = rte_msg_get_dest(msg_bridge->c_msg);
  assert(dest_locs);

  const uint32_t arr_len = rte_list_size(dest_locs);

  napi_value js_dest_locs = NULL;
  status = napi_create_array_with_length(env, arr_len, &js_dest_locs);
  RETURN_UNDEFINED_IF_NAPI_FAIL(status == napi_ok && js_dest_locs != NULL,
                                "Failed to create JS array with size %u: %d",
                                arr_len, status);

  rte_list_foreach (dest_locs, iter) {
    rte_loc_t* dest_loc = rte_ptr_listnode_get(iter.node);
    assert(dest_loc);

    rte_string_t* dest_loc_str = rte_string_create();
    assert(dest_loc_str);

    rte_loc_to_json_string(dest_loc, dest_loc_str);

    napi_value dest_loc_js = NULL;
    status =
        napi_create_string_utf8(env, rte_string_c_str(dest_loc_str),
                                rte_string_len(dest_loc_str), &dest_loc_js);

    rte_string_destroy(dest_loc_str);

    RETURN_UNDEFINED_IF_NAPI_FAIL(status == napi_ok,
                                  "Failed to create JS string: %d", status);

    napi_set_element(env, js_dest_locs, iter.index, dest_loc_js);
  }

  return js_dest_locs;
}

void rte_nodejs_msg_init_from_c_msg(rte_nodejs_msg_t* self,
                                    rte_sharedptr_t* c_msg) {
  assert(self);
  assert(c_msg && rte_msg_check_integrity(c_msg));

  rte_signature_set(&self->signature, RTE_NODEJS_MSG_SIGNATURE);
  rte_sanitizer_thread_check_init_with_current_thread(&self->thread_check);

  self->js_ref = NULL;

  self->c_msg = c_msg;
  rte_sharedptr_inc_rc(self->c_msg);
}

void rte_nodejs_msg_deinit(rte_nodejs_msg_t* self) {
  assert(self && rte_nodejs_msg_check_integrity(self, true));
  rte_sanitizer_thread_check_deinit(&self->thread_check);
}

static void rte_nodejs_msg_destroy(rte_nodejs_msg_t* self) {
  assert(self && rte_nodejs_msg_check_integrity(self, true));

  if (self->c_msg) {
    // Because the JS msg is destroyed, so it releases one ownership of the
    // bridge.
    self->c_msg = rte_sharedptr_dec_rc(self->c_msg);
  }

  rte_nodejs_msg_deinit(self);
  rte_free(self);
}

void rte_nodejs_msg_take_ownership_from_js(rte_nodejs_msg_t* self) {
  assert(self && rte_nodejs_msg_check_integrity(self, true));
  self->shared_ptr = rte_sharedptr_create(self, rte_nodejs_msg_destroy);
}

void rte_nodejs_msg_release_ownership_from_js(rte_nodejs_msg_t* self) {
  assert(self && rte_nodejs_msg_check_integrity(self, true));
  rte_sharedptr_dec_rc(self->shared_ptr);
}

void rte_nodejs_msg_finalize(napi_env env, rte_nodejs_msg_t* self) {
  assert(self && rte_nodejs_msg_check_integrity(self, true));

  // The JS msg is finalized, so delete the reference to it.
  napi_delete_reference(env, self->js_ref);
}

napi_value rte_nodejs_msg_module_init(napi_env env, napi_value exports) {
  assert(env && exports);

  EXPORT_FUNC(env, exports, rte_nodejs_msg_get_src_extension);
  EXPORT_FUNC(env, exports, rte_nodejs_msg_set_dest);
  EXPORT_FUNC(env, exports, rte_nodejs_msg_set_property);
  EXPORT_FUNC(env, exports, rte_nodejs_msg_get_property);
  EXPORT_FUNC(env, exports, rte_nodejs_msg_get_src_loc);
  EXPORT_FUNC(env, exports, rte_nodejs_msg_get_dest_locs);

  return exports;
}
