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

#include <assert.h>

#include "js_native_api.h"
#include "lib/mutex.h"
#include "lib/shared_ptr.h"
#include "lib/signature.h"
#include "node_api_types.h"
#include "rte_runtime/binding/nodejs/src/common/common.h"
#include "rte_runtime/sanitizer/thread_check.h"

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

bool rte_nodejs_threadsafe_function_check_integrity(
    rte_nodejs_threadsafe_function_t* self, bool check_thread) {
  assert(self);

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

static rte_nodejs_threadsafe_function_t*
rte_nodejs_threadsafe_function_create_empty(void) {
  rte_nodejs_threadsafe_function_t* self =
      rte_malloc(sizeof(rte_nodejs_threadsafe_function_t));
  assert(self);

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

  rte_string_init(&self->name);
  self->js_func_ref = NULL;
  self->tsfn = NULL;
  self->lock = rte_mutex_create();

  return self;
}

void rte_nodejs_threadsafe_function_inc_rc(
    rte_nodejs_threadsafe_function_t* self) {
  assert(self &&
         // RTE_NOLINTNEXTLINE(thread-check)
         // thread-check: this function is meant to be called in any threads,
         // and all operations in this function is thread-safe.
         rte_nodejs_threadsafe_function_check_integrity(self, false));

  rte_sharedptr_inc_rc(self->shared_ptr);
}

void rte_nodejs_threadsafe_function_dec_rc(
    rte_nodejs_threadsafe_function_t* self) {
  assert(self &&
         // RTE_NOLINTNEXTLINE(thread-check)
         // thread-check: this function is meant to be called in any threads,
         // and all operations in this function is thread-safe.
         rte_nodejs_threadsafe_function_check_integrity(self, false));

  rte_sharedptr_dec_rc(self->shared_ptr);
}

static void rte_nodejs_threadsafe_function_finalize(
    UNUSED napi_env env, void* finalize_data, UNUSED void* finalize_hint) {
  assert(env);

  rte_nodejs_threadsafe_function_t* tsfn_bridge = finalize_data;
  assert(tsfn_bridge &&
         rte_nodejs_threadsafe_function_check_integrity(tsfn_bridge, true));

  ZF_LOGV("RTE JS threadsafe function %s is finalized.",
          rte_string_c_str(&tsfn_bridge->name));

  // The tsfn would be accessed from the native part, so we need to protect the
  // operations of it.
  rte_mutex_lock(tsfn_bridge->lock);
  // Indicate that the tsfn JS part is disappear.
  tsfn_bridge->tsfn = NULL;
  rte_mutex_unlock(tsfn_bridge->lock);

  // Release one napi reference to the JS function which this tsfn points to.
  assert(tsfn_bridge->js_func_ref);
  napi_status status =
      napi_reference_unref(env, tsfn_bridge->js_func_ref, NULL);
  ASSERT_IF_NAPI_FAIL(status == napi_ok,
                      "Failed to release napi reference to the JS function "
                      "which the tsfn %s points to.",
                      rte_string_c_str(&tsfn_bridge->name));

  // Decrement the shared_ptr reference count to indicate that the JS part of
  // the bridge is disappear.
  rte_sharedptr_dec_rc(tsfn_bridge->shared_ptr);
}

static void rte_nodejs_threadsafe_function_destroy(
    rte_nodejs_threadsafe_function_t* self) {
  assert(self &&
         // RTE_NOLINTNEXTLINE(thread-check)
         // thread-check: if reaching here, it means all the user of the tsfn
         // has ended, so it's safe to call this function in any threads.
         rte_nodejs_threadsafe_function_check_integrity(self, false) &&
         // Before being destroyed, the threadsafe function should have already
         // been finalized.
         !self->tsfn);

  rte_string_deinit(&self->name);
  if (self->lock) {
    rte_mutex_destroy(self->lock);
    self->lock = NULL;
  }

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

/**
 * @brief Wrap a javascript function 'fn' to be called from the native part.
 */
rte_nodejs_threadsafe_function_t* rte_nodejs_threadsafe_function_create(
    napi_env env, const char* name, napi_value js_func,
    napi_threadsafe_function_call_js invoke_js) {
  assert(name);
  assert(js_func);

  rte_nodejs_threadsafe_function_t* self =
      rte_nodejs_threadsafe_function_create_empty();

  napi_status status = napi_ok;

  // Create a JS reference to keep the JS function alive.
  status = napi_create_reference(env, js_func, 1, &self->js_func_ref);
  ASSERT_IF_NAPI_FAIL(status == napi_ok && self->js_func_ref != NULL,
                      "Failed to create reference to JS function function: %d",
                      status);

  // Create a name to represent this work. This is must, otherwise,
  // 'napi_create_threadsafe_function' will fail because of this.
  napi_value work_name = NULL;
  status = napi_create_string_utf8(env, name, strlen(name), &work_name);
  ASSERT_IF_NAPI_FAIL(status == napi_ok, "Failed to create JS string: %d",
                      status);

  rte_string_init_with_value(&self->name, "%s", name);

  // Create a threadsafe function for the corresponding javascript function
  // 'js_func'.
  status = napi_create_threadsafe_function(
      env, js_func, NULL, work_name, 0, 1, self,
      rte_nodejs_threadsafe_function_finalize, NULL, invoke_js, &self->tsfn);
  ASSERT_IF_NAPI_FAIL(status == napi_ok,
                      "Failed to create JS threadsafe function: %d", status);

  // Indicate that the JS part takes one ownership of this tsfn bridge.
  self->shared_ptr =
      rte_sharedptr_create(self, rte_nodejs_threadsafe_function_destroy);

  return self;
}

bool rte_nodejs_threadsafe_function_invoke(
    rte_nodejs_threadsafe_function_t* self, void* data) {
  assert(self);
  // RTE_NOLINTNEXTLINE(thread-check)
  // thread-check: this function is meant to be called in all threads.
  assert(rte_nodejs_threadsafe_function_check_integrity(self, false));

  bool result = true;

  // Because tsfn_bridge would be accessed in the JS main thread at any time, so
  // we need a locking here.
  rte_mutex_lock(self->lock);

  if (self->tsfn == NULL) {
    ZF_LOGW(
        "Failed to callback to JS function, because the JS function has been "
        "disappear.");

    result = false;
    goto done;
  }

  napi_status status =
      napi_call_threadsafe_function(self->tsfn, data, napi_tsfn_blocking);
  if (status != napi_ok) {
    ZF_LOGE("Failed to callback to JS function %s: status: %d",
            rte_string_c_str(&self->name), status);

    result = false;
    goto done;
  }

done:
  rte_mutex_unlock(self->lock);
  return result;
}

void rte_nodejs_threadsafe_function_release(
    napi_env env, rte_nodejs_threadsafe_function_t* self) {
  assert(env);
  assert(self && rte_nodejs_threadsafe_function_check_integrity(self, true));

  ZF_LOGV("Release JS threadsafe function: %s", rte_string_c_str(&self->name));

  // 'releasing' the threadsafe function, so that it can be garbage collected.
  napi_status status =
      napi_release_threadsafe_function(self->tsfn, napi_tsfn_abort);
  ASSERT_IF_NAPI_FAIL(status == napi_ok,
                      "Failed to release JS threadsafe function: %d", status);
}
