/**
 *
 * 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/metadata/metadata.h"

#include <assert.h>
#include <stdbool.h>
#include <stdlib.h>

#include "io/network.h"
#include "js_native_api.h"
#include "lib/alloc.h"
#include "lib/shared_ptr.h"
#include "lib/signature.h"
#include "macro/mark.h"
#include "rte_runtime/binding/nodejs/src/common/common.h"
#include "rte_runtime/metadata/metadata.h"
#include "rte_runtime/rte/rte.h"
#include "rte_runtime/sanitizer/thread_check.h"
#include "third_party/mxml-2/mmd.h"

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

static napi_ref js_metadata_info_constructor_ref = NULL;  // NOLINT

UNUSED static bool rte_nodejs_metadata_info_check_integrity(
    rte_nodejs_metadata_info_t* self, bool check_thread) {
  assert(self);

  if (rte_signature_get(&self->signature) !=
      RTE_NODEJS_METADATA_INFO_SIGNATURE) {
    return false;
  }
  if (check_thread &&
      !rte_sanitizer_thread_check_do_check(&self->thread_check)) {
    return false;
  }
  return true;
}

/**
 * @brief This function will be called when the JS metadata info instance is
 * garbage collected.
 */
static void rte_nodejs_metadata_info_finalize(napi_env env, void* data,
                                              UNUSED void* hint) {
  assert(env);

  ZF_LOGV("JS JS metadata_info is finalized.");

  rte_nodejs_metadata_info_t* metadata_info_bridge = data;
  assert(metadata_info_bridge &&
         rte_nodejs_metadata_info_check_integrity(metadata_info_bridge, true));

  // According to NAPI doc:
  // ====
  // Caution: The optional returned reference (if obtained) should be deleted
  // via napi_delete_reference __ONLY__ in response to the finalize callback
  // invocation. If it is deleted before then, then the finalize callback may
  // never be invoked. Therefore, when obtaining a reference a finalize callback
  // is also required in order to enable correct disposal of the reference.
  napi_status status = napi_delete_reference(env, metadata_info_bridge->js_ref);
  ASSERT_IF_NAPI_FAIL(status == napi_ok,
                      "Failed to delete reference of JS metadata: %d", status);

  // Decrement the reference count to indicate that the JS part is disappeared.
  rte_sharedptr_dec_rc(metadata_info_bridge->shared_ptr);
}

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

  const size_t argc = 1;
  napi_value args[argc];  // MetadataInfo
  UNUSED bool rc = rte_nodejs_get_args(env, info, args, argc);
  assert(rc);

  // Remember JS metadata_info constructor.
  UNUSED napi_status status =
      napi_create_reference(env, args[0], 1, &js_metadata_info_constructor_ref);
  ASSERT_IF_NAPI_FAIL(
      status == napi_ok,
      "Failed to create C reference to JS metadata_info constructor: %d",
      status);

  return UNDEFINED(env);
}

static void rte_nodejs_metadata_info_destroy(void* metadata_info_bridge_) {
  rte_nodejs_metadata_info_t* metadata_info_bridge = metadata_info_bridge_;
  assert(metadata_info_bridge &&
         rte_nodejs_metadata_info_check_integrity(metadata_info_bridge, true));

  rte_sanitizer_thread_check_deinit(&metadata_info_bridge->thread_check);
  rte_free(metadata_info_bridge);
}

static void rte_nodejs_metadata_info_destroy_c_part(
    void* metadata_info_bridge_) {
  rte_nodejs_metadata_info_t* metadata_info_bridge = metadata_info_bridge_;
  assert(metadata_info_bridge &&
         // RTE_NOLINTNEXTLINE(thread-check)
         // thread-check: in the case of addon, because JS addons are registered
         // in a standalone registration thread, so its on_init() will be called
         // in that thread, too. All the following operations in this function
         // are thread-safe, so this function is thread-safe, too.
         rte_nodejs_metadata_info_check_integrity(metadata_info_bridge, false));

  // Decrement the reference count to indicate that the C part is disappeared.
  rte_sharedptr_dec_rc(metadata_info_bridge->shared_ptr);
}

napi_value rte_nodejs_metadata_info_wrap(napi_env env,
                                         rte_metadata_info_t* metadata_info) {
  assert(env && metadata_info);

  rte_nodejs_metadata_info_t* metadata_info_bridge =
      rte_malloc(sizeof(rte_nodejs_metadata_info_t));
  assert(metadata_info_bridge);

  rte_signature_set(&metadata_info_bridge->signature,
                    RTE_NODEJS_METADATA_INFO_SIGNATURE);
  rte_sanitizer_thread_check_init_with_current_thread(
      &metadata_info_bridge->thread_check);

  metadata_info_bridge->c_metadata_info = metadata_info;
  metadata_info_bridge->js_ref = NULL;

  // The initial reference count (1) indicates that the C metadata_info
  // references to the bridge.
  metadata_info_bridge->shared_ptr = rte_sharedptr_create(
      metadata_info_bridge, rte_nodejs_metadata_info_destroy);

  rte_metadata_info_set_me_in_target_lang(metadata_info, metadata_info_bridge);
  rte_metadata_info_set_destroy_handler_in_target_lang(
      metadata_info, rte_nodejs_metadata_info_destroy_c_part);

  napi_value js_metadata_info = rte_nodejs_create_new_js_object_and_wrap(
      env, js_metadata_info_constructor_ref, metadata_info_bridge,
      rte_nodejs_metadata_info_finalize, &metadata_info_bridge->js_ref);

  // Increment the napi reference count, so that it will not be garbage
  // collected before we are done with it.
  uint32_t ref_cnt = 0;
  napi_status status =
      napi_reference_ref(env, metadata_info_bridge->js_ref, &ref_cnt);
  ASSERT_IF_NAPI_FAIL(status == napi_ok && ref_cnt == 1,
                      "Failed to increment JS metadata_info reference: %d",
                      status);

  // Increment the sharedptr reference count to indicate that the JS
  // metadata_info references to the bridge.
  rte_sharedptr_inc_rc(metadata_info_bridge->shared_ptr);

  return js_metadata_info;
}

/**
 * @brief This function will be called when the JS metadata_info is not needed
 * anymore.
 */
void rte_nodejs_metadata_info_invalidate_js(napi_env env,
                                            napi_value js_metadata) {
  assert(env && js_metadata);

  ZF_LOGV("Invalidate JS metadata_info.");

  rte_nodejs_metadata_info_t* metadata_info_bridge = NULL;
  napi_status status =
      napi_unwrap(env, js_metadata, (void**)&metadata_info_bridge);
  ASSERT_IF_NAPI_FAIL(status == napi_ok && metadata_info_bridge != NULL,
                      "Failed to get metadata_info bridge: %d", status);

  // Decrement the napi reference count, so that JS metadata_info instance could
  // be garbage collected.
  uint32_t ref_cnt = 0;
  status = napi_reference_unref(env, metadata_info_bridge->js_ref, &ref_cnt);
  ASSERT_IF_NAPI_FAIL(status == napi_ok && ref_cnt == 0,
                      "Failed to decrement the JS metadata_info reference: %d",
                      status);
}

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

  rte_string_t metadata_str;
  rte_string_init(&metadata_str);

  const size_t argc = 3;
  napi_value args[argc];  // this, type, string
  if (!rte_nodejs_get_args(env, info, args, argc)) {
    goto done;
  }

  rte_nodejs_metadata_info_t* metadata_info_bridge = NULL;
  napi_status status = napi_unwrap(env, args[0], (void**)&metadata_info_bridge);
  ASSERT_IF_NAPI_FAIL(status == napi_ok && metadata_info_bridge != NULL,
                      "Failed to get metadata_info bridge: %d", status);

  int32_t type_int = 0;
  status = napi_get_value_int32(env, args[1], &type_int);
  ASSERT_IF_NAPI_FAIL(
      status == napi_ok && type_int > 0 && type_int <= RTE_METADATA_TYPE_MAX,
      "Failed to get metadata_info type: %d", status);

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

  assert(
      metadata_info_bridge->c_metadata_info &&
      rte_metadata_info_check_integrity(metadata_info_bridge->c_metadata_info));

  rte_metadata_info_set(metadata_info_bridge->c_metadata_info, type_int,
                        rte_string_c_str(&metadata_str));

done:
  rte_string_deinit(&metadata_str);

  return UNDEFINED(env);
}

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

  EXPORT_FUNC(env, exports, rte_nodejs_metadata_info_set);
  EXPORT_FUNC(env, exports, rte_nodejs_metadata_info_register_class);

  return exports;
}
