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

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

#include "lib/alloc.h"
#include "lib/list.h"
#include "lib/list_node.h"
#include "lib/shared_ptr.h"
#include "lib/signature.h"
#include "rte_runtime/binding/nodejs/src/common/common.h"
#include "rte_runtime/binding/nodejs/src/common/semaphore.h"
#include "rte_runtime/binding/nodejs/src/common/tsfn.h"
#include "rte_runtime/binding/nodejs/src/extension/extension.h"
#include "rte_runtime/binding/nodejs/src/metadata/metadata.h"
#include "rte_runtime/binding/nodejs/src/rte/rte.h"
#include "rte_runtime/extension/extension.h"
#include "rte_runtime/extension_group/extension_group.h"
#include "rte_runtime/metadata/metadata.h"
#include "rte_runtime/rte/rte.h"
#include "rte_runtime/sanitizer/thread_check.h"

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

#define GET_JS_EXTENSION_GROUP_AND_JS_RTE_FROM_BRIDGE(                         \
    env, bridge, js_extension_group, js_rte)                                   \
  do {                                                                         \
    napi_status status = napi_get_reference_value((env), (bridge)->js_ref,     \
                                                  &(js_extension_group));      \
    GOTO_LABEL_IF_NAPI_FAIL(done,                                              \
                            status == napi_ok && js_extension_group != NULL,   \
                            "Failed to get JS extension group: %d", status);   \
                                                                               \
    status = napi_get_reference_value((env), (bridge)->js_rte_ref, &(js_rte)); \
    GOTO_LABEL_IF_NAPI_FAIL(done, status == napi_ok && js_rte != NULL,         \
                            "Failed to get JS rte: %d", status);               \
  } while (0)

typedef struct on_init_call_info_t {
  rte_nodejs_extension_group_t* extension_group_bridge;
  rte_t* rte;
  rte_metadata_info_t* manifest;
  rte_metadata_info_t* property;
  rte_nodejs_sem_t sem;
  bool js_result;
} on_init_call_info_t;

typedef struct on_deinit_call_info_t {
  rte_nodejs_extension_group_t* extension_group_bridge;
  rte_t* rte;
  rte_nodejs_sem_t sem;
  bool js_result;
} on_deinit_call_info_t;

typedef struct on_create_extension_call_info_t {
  rte_nodejs_extension_group_t* extension_group_bridge;
  rte_list_t* result;
  rte_nodejs_sem_t sem;
  bool success;
  bool js_result;
} on_create_extension_call_info_t;

typedef struct on_delete_extension_call_info_t {
  rte_nodejs_extension_group_t* extension_group_bridge;
  rte_list_t* extensions;
  rte_nodejs_sem_t sem;
  bool js_result;
} on_delete_extension_call_info_t;

bool rte_nodejs_extension_group_check_integrity(
    rte_nodejs_extension_group_t* self, bool check_thread) {
  assert(self);

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

static void invoke_extension_group_js_on_init(napi_env env, napi_value fn,
                                              UNUSED void* context,
                                              void* data) {
  on_init_call_info_t* call_info = data;
  assert(call_info);

  napi_value js_rte = rte_nodejs_rte_wrap(env, call_info->rte, NULL);
  assert(js_rte);

  assert(call_info->manifest &&
         rte_metadata_info_check_integrity(call_info->manifest));
  napi_value js_manifest =
      rte_nodejs_metadata_info_wrap(env, call_info->manifest);
  assert(js_manifest);

  assert(call_info->property &&
         rte_metadata_info_check_integrity(call_info->property));
  napi_value js_property =
      rte_nodejs_metadata_info_wrap(env, call_info->property);
  assert(js_property);

  napi_status status = napi_ok;

  // There might be no JS references to the JS 'rte' object in the JS world,
  // so we need to take a reference of it.
  if (!call_info->extension_group_bridge->js_rte_ref) {
    status = napi_create_reference(
        env, js_rte, 1, &call_info->extension_group_bridge->js_rte_ref);
    ASSERT_IF_NAPI_FAIL(status == napi_ok,
                        "Failed to create C reference to JS 'rte': %d", status);
  }

  napi_value js_extension_group = NULL;
  status = napi_get_reference_value(
      env, call_info->extension_group_bridge->js_ref, &js_extension_group);
  ASSERT_IF_NAPI_FAIL(status == napi_ok && js_extension_group != NULL,
                      "Failed to get JS extension group: %d", status);

  napi_value result = NULL;
  napi_value argv[] = {js_rte, js_manifest, js_property};
  status = napi_call_function(env, js_extension_group, fn, 3, argv, &result);
  GOTO_LABEL_IF_NAPI_FAIL(error, status == napi_ok,
                          "Failed to call JS extension group on_init(): %d",
                          status);

  goto done;

error:
  rte_nodejs_metadata_info_invalidate_js(env, js_manifest);
  rte_nodejs_metadata_info_invalidate_js(env, js_property);

done:
  rte_nodejs_sem_signal(&call_info->sem);
}

static void proxy_on_init(rte_extension_group_t* extension_group, rte_t* rte,
                          rte_metadata_info_t* manifest,
                          rte_metadata_info_t* property) {
  assert(extension_group &&
         // RTE_NOLINTNEXTLINE(thread-check)
         // thread-check: the extension_group_bridge pointer should unchanged
         // during this function is running, so it's thread safe.
         rte_extension_group_check_integrity(extension_group, false));
  assert(rte && rte_check_integrity(rte));

  rte_nodejs_extension_group_t* extension_group_bridge =
      rte_extension_group_get_me_in_target_lang(extension_group);
  assert(extension_group_bridge &&
         // RTE_NOLINTNEXTLINE(thread-check)
         // thread-check: in order to maintain thread safety, we use semaphore
         // below to prevent JS main thread and the RTE extension thread access
         // the extension group bridge at the same time.
         rte_nodejs_extension_group_check_integrity(extension_group_bridge,
                                                    false));

  on_init_call_info_t call_info = {
      extension_group_bridge, rte, manifest, property, {0}, .js_result = true,
  };

  PREPARE_TO_CALL_TSFN(call_info);

  bool rc = rte_nodejs_threadsafe_function_invoke(
      extension_group_bridge->js_on_init, &call_info);

  END_TO_CALL_TSFN(call_info, "Failed to call extension_group on_init().");

  if (!rc || !call_info.js_result) {
    rte_on_init_done(rte, manifest, property);
  }
}

static void invoke_extension_group_js_on_deinit(napi_env env, napi_value fn,
                                                UNUSED void* context,
                                                void* data) {
  on_deinit_call_info_t* call_info = data;
  assert(call_info);
  assert(call_info->extension_group_bridge &&
         rte_nodejs_extension_group_check_integrity(
             call_info->extension_group_bridge, true));

  napi_status status = napi_ok;

  napi_value js_extension_group = NULL;
  napi_value js_rte = NULL;
  GET_JS_EXTENSION_GROUP_AND_JS_RTE_FROM_BRIDGE(
      env, call_info->extension_group_bridge, js_extension_group, js_rte);

  napi_value result = NULL;
  status = napi_call_function(env, js_extension_group, fn, 1, &js_rte, &result);
  CHECK_IF_NAPI_FAIL(status == napi_ok,
                     "Failed to call JS extension group on_deinit().");

done:
  rte_nodejs_sem_signal(&call_info->sem);
}

static void proxy_on_deinit(rte_extension_group_t* extension_group,
                            rte_t* rte) {
  assert(extension_group &&
         // RTE_NOLINTNEXTLINE(thread-check)
         // thread-check: the extension_group_bridge pointer should unchanged
         // during this function is running, so it's thread safe.
         rte_extension_group_check_integrity(extension_group, false));
  assert(rte && rte_check_integrity(rte));

  rte_nodejs_extension_group_t* extension_group_bridge =
      rte_extension_group_get_me_in_target_lang(extension_group);
  assert(extension_group_bridge &&
         // RTE_NOLINTNEXTLINE(thread-check)
         // thread-check: in order to maintain thread safety, we use semaphore
         // below to prevent JS main thread and the RTE extension thread access
         // the extension group bridge at the same time.
         rte_nodejs_extension_group_check_integrity(extension_group_bridge,
                                                    false));

  on_deinit_call_info_t call_info = {
      extension_group_bridge,
      rte,
      {0},
      .js_result = true,
  };

  PREPARE_TO_CALL_TSFN(call_info);

  bool rc = rte_nodejs_threadsafe_function_invoke(
      extension_group_bridge->js_on_deinit, &call_info);

  END_TO_CALL_TSFN(call_info, "Failed to call extension_group on_deinit().");

  if (!rc || !call_info.js_result) {
    rte_on_deinit_done(rte);
  }
}

static void rte_nodejs_extension_group_release_callbacks(
    napi_env env, rte_nodejs_extension_group_t* self) {
  assert(env && self && rte_nodejs_extension_group_check_integrity(self, true));

  rte_nodejs_threadsafe_function_release(env, self->js_on_init);
  rte_nodejs_threadsafe_function_release(env, self->js_on_deinit);
  rte_nodejs_threadsafe_function_release(env, self->js_on_create_extension);
  rte_nodejs_threadsafe_function_release(env, self->js_on_delete_extension);
}

static void invoke_extension_group_js_on_create_extension(napi_env env,
                                                          napi_value fn,
                                                          UNUSED void* context,
                                                          void* data) {
  assert(env);
  assert(fn);

  on_create_extension_call_info_t* call_info = data;
  assert(call_info);
  assert(call_info->extension_group_bridge &&
         rte_nodejs_extension_group_check_integrity(
             call_info->extension_group_bridge, true));

  napi_value js_extension_group = NULL;
  napi_value js_rte = NULL;
  GET_JS_EXTENSION_GROUP_AND_JS_RTE_FROM_BRIDGE(
      env, call_info->extension_group_bridge, js_extension_group, js_rte);

  call_info->success = true;

  napi_value result = NULL;
  napi_status status =
      napi_call_function(env, js_extension_group, fn, 1, &js_rte, &result);
  GOTO_LABEL_IF_NAPI_FAIL(
      error, status == napi_ok,
      "Failed to call JS extension group on_create_extension(): %d", status);

  goto done;

error:
  call_info->success = false;

done:
  rte_nodejs_sem_signal(&call_info->sem);
}

static void proxy_on_create_extension(rte_extension_group_t* extension_group,
                                      rte_t* rte) {
  assert(extension_group &&
         // RTE_NOLINTNEXTLINE(thread-check)
         // thread-check: the extension_group_bridge pointer should unchanged
         // during this function is running, so it's thread safe.
         rte_extension_group_check_integrity(extension_group, false));
  assert(rte && rte_check_integrity(rte));
  assert(rte == extension_group->rte);
  assert(rte->attach_to_type == RTE_ATTACH_TO_EXTENSION_GROUP);
  assert(rte->attach_to.extension_group == extension_group);

  rte_nodejs_extension_group_t* extension_group_bridge =
      rte_extension_group_get_me_in_target_lang(extension_group);
  assert(extension_group_bridge &&
         // RTE_NOLINTNEXTLINE(thread-check)
         // thread-check: in order to maintain thread safety, we use semaphore
         // below to prevent JS main thread and the RTE extension thread access
         // the extension group bridge at the same time.
         rte_nodejs_extension_group_check_integrity(extension_group_bridge,
                                                    false));

  on_create_extension_call_info_t call_info = {
      .extension_group_bridge = extension_group_bridge,
      .result = NULL,
      .success = false,
      .js_result = true,
  };

  PREPARE_TO_CALL_TSFN(call_info);

  bool rc = rte_nodejs_threadsafe_function_invoke(
      extension_group_bridge->js_on_create_extension, &call_info);

  END_TO_CALL_TSFN(call_info,
                   "Failed to call extension_group on_create_extension().");

  if (!rc || !call_info.js_result) {
    rte_list_t extensions;
    rte_list_init(&extensions);
    rte_on_create_extension_done(rte, extensions);
    rte_list_clear(&extensions);
  }
}

static void invoke_extension_group_js_on_delete_extension(napi_env env,
                                                          napi_value fn,
                                                          UNUSED void* context,
                                                          void* data) {
  on_delete_extension_call_info_t* call_info = data;
  assert(call_info);
  assert(call_info->extension_group_bridge &&
         rte_nodejs_extension_group_check_integrity(
             call_info->extension_group_bridge, true));

  const uint32_t arr_len = rte_list_size(call_info->extensions);

  napi_value js_extensions = NULL;
  napi_status status =
      napi_create_array_with_length(env, arr_len, &js_extensions);
  GOTO_LABEL_IF_NAPI_FAIL(done, status == napi_ok,
                          "Failed to create JS array with length %d: %d",
                          arr_len, status);

  rte_list_foreach (call_info->extensions, iter) {
    rte_extension_t* c_extension = rte_ptr_listnode_get(iter.node);
    assert(c_extension && rte_extension_check_integrity(c_extension));

    rte_nodejs_extension_t* extension_bridge =
        rte_extension_get_me_in_target_lang(c_extension);
    assert(extension_bridge &&
           rte_nodejs_extension_check_integrity(extension_bridge, true));

    napi_value js_extension = NULL;
    status =
        napi_get_reference_value(env, extension_bridge->js_ref, &js_extension);
    GOTO_LABEL_IF_NAPI_FAIL(done, status == napi_ok && js_extension != NULL,
                            "Failed to get JS extension: %d", status);

    napi_set_element(env, js_extensions, iter.index, js_extension);
  }

  napi_value js_extension_group = NULL;
  napi_value js_rte = NULL;
  GET_JS_EXTENSION_GROUP_AND_JS_RTE_FROM_BRIDGE(
      env, call_info->extension_group_bridge, js_extension_group, js_rte);

  napi_value argv[] = {js_rte, js_extensions};
  status = napi_call_function(env, js_extension_group, fn, 2, argv, NULL);
  GOTO_LABEL_IF_NAPI_FAIL(
      done, status == napi_ok,
      "Failed to call JS extension group delete_extension(): %d", status);

  // Decrease ref on all extensions.
  rte_list_foreach (call_info->extensions, iter) {
    rte_extension_t* c_extension = rte_ptr_listnode_get(iter.node);
    assert(c_extension && rte_extension_check_integrity(c_extension));

    rte_nodejs_extension_t* extension_bridge =
        rte_extension_get_me_in_target_lang(c_extension);
    assert(extension_bridge &&
           rte_nodejs_extension_check_integrity(extension_bridge, true));

    // Note: threadsafe functions are called in queue, so following operations
    // are serialized.
    uint32_t rc = 0;
    status = napi_reference_unref(env, extension_bridge->js_ref, &rc);
    GOTO_LABEL_IF_NAPI_FAIL(done, status == napi_ok,
                            "Failed to unref JS extension: %d", status);

    // Indicate that the extension group doesn't hold an ownership of the
    // extension bridge.
    //
    //                             addon
    //                               |
    //                               v
    //   JS extension         extension_bridge         C extension
    //         o      ==x==>         o         <==          o
    //                               ^
    //                               x
    //                               |
    //                             group
    //
    rte_sharedptr_dec_rc(extension_bridge->shared_ptr);

    rte_extension_destroy(c_extension);
  }

done:
  rte_nodejs_sem_signal(&call_info->sem);
}

static void proxy_on_delete_extension(rte_extension_group_t* extension_group,
                                      rte_t* rte, rte_list_t extensions) {
  assert(extension_group &&
         // RTE_NOLINTNEXTLINE(thread-check)
         // thread-check: the extension_group_bridge pointer should unchanged
         // during this function is running, so it's thread safe.
         rte_extension_group_check_integrity(extension_group, false));
  assert(rte && rte_check_integrity(rte));

  rte_nodejs_extension_group_t* extension_group_bridge =
      rte_extension_group_get_me_in_target_lang(extension_group);
  assert(extension_group_bridge &&
         // RTE_NOLINTNEXTLINE(thread-check)
         // thread-check: in order to maintain thread safety, we use semaphore
         // below to prevent JS main thread and the RTE extension thread access
         // the extension group bridge at the same time.
         rte_nodejs_extension_group_check_integrity(extension_group_bridge,
                                                    false));

  on_delete_extension_call_info_t call_info = {
      .extension_group_bridge = extension_group_bridge,
      .extensions = &extensions,
      .js_result = true,
  };

  PREPARE_TO_CALL_TSFN(call_info);

  bool rc = rte_nodejs_threadsafe_function_invoke(
      extension_group_bridge->js_on_delete_extension, &call_info);

  END_TO_CALL_TSFN(call_info,
                   "Failed to call extension_group on_delete_extension().");

  if (!rc || !call_info.js_result) {
    rte_on_delete_extension_done(rte);
  }
}

static void rte_nodejs_extension_group_detach_callbacks(
    rte_nodejs_extension_group_t* self) {
  assert(self);

  rte_nodejs_threadsafe_function_dec_rc(self->js_on_init);
  rte_nodejs_threadsafe_function_dec_rc(self->js_on_deinit);
  rte_nodejs_threadsafe_function_dec_rc(self->js_on_create_extension);
  rte_nodejs_threadsafe_function_dec_rc(self->js_on_delete_extension);
}

static void rte_nodejs_extension_group_finalize(napi_env env, void* data,
                                                UNUSED void* hint) {
  rte_nodejs_extension_group_t* extension_group_bridge = data;
  assert(extension_group_bridge && rte_nodejs_extension_group_check_integrity(
                                       extension_group_bridge, true));

  ZF_LOGV("RTE JS extension_group is finalized.");

  // Unreference the JS rte instance to prevent it from not being garbage
  // collected.
  uint32_t rte_ref_count = 0;
  UNUSED napi_status status = napi_reference_unref(
      env, extension_group_bridge->js_rte_ref, &rte_ref_count);
  ASSERT_IF_NAPI_FAIL(status == napi_ok && rte_ref_count == 0,
                      "Failed to unref JS rte reference in extension group: %d",
                      status);

  // The JS extension group is finalized, so delete the reference to it.
  napi_delete_reference(env, extension_group_bridge->js_ref);

  // The JS extension group is finalized, so decrease the reference count to the
  // extension group bridge to indicate that one of the ownership of the
  // extension group bridge will not be hold by the JS extension group.
  rte_sharedptr_dec_rc(extension_group_bridge->shared_ptr);
}

static void rte_nodejs_extension_group_attach_callbacks(
    napi_env env, rte_nodejs_extension_group_t* self) {
  assert(env);
  assert(self);

  napi_value js_extension_group = NULL;
  napi_status status =
      napi_get_reference_value(env, self->js_ref, (void*)&js_extension_group);
  ASSERT_IF_NAPI_FAIL(status == napi_ok && js_extension_group != NULL,
                      "Failed to get JS extension group: %d", status);

  napi_value js_cb_on_init =
      rte_nodejs_get_property(env, js_extension_group, "_onInit");
  CREATE_JS_CALLBACK(self->js_on_init, env, "ExtensionGroup::onInit",
                     js_cb_on_init, invoke_extension_group_js_on_init);

  napi_value js_cb_on_deinit =
      rte_nodejs_get_property(env, js_extension_group, "_onDeinit");
  CREATE_JS_CALLBACK(self->js_on_deinit, env, "ExtensionGroup::onDeinit",
                     js_cb_on_deinit, invoke_extension_group_js_on_deinit);

  napi_value js_cb_on_create_extension =
      rte_nodejs_get_property(env, js_extension_group, "_onCreateExtension");
  CREATE_JS_CALLBACK(
      self->js_on_create_extension, env, "ExtensionGroup::onCreateExtension",
      js_cb_on_create_extension, invoke_extension_group_js_on_create_extension);

  napi_value js_cb_on_delete_extension =
      rte_nodejs_get_property(env, js_extension_group, "_onDeleteExtension");
  CREATE_JS_CALLBACK(
      self->js_on_delete_extension, env, "ExtensionGroup::onDeleteExtension",
      js_cb_on_delete_extension, invoke_extension_group_js_on_delete_extension);
}

static void rte_nodejs_extension_group_destroy(
    rte_nodejs_extension_group_t* self) {
  assert(self && rte_nodejs_extension_group_check_integrity(self, true));

  rte_nodejs_extension_group_detach_callbacks(self);

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

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

  rte_string_t name;
  rte_string_init(&name);

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

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

  rte_nodejs_extension_group_t* extension_group_bridge =
      (rte_nodejs_extension_group_t*)rte_malloc(
          sizeof(rte_nodejs_extension_group_t));
  assert(extension_group_bridge);
  memset(extension_group_bridge, 0, sizeof(rte_nodejs_extension_group_t));

  rte_signature_set(&extension_group_bridge->signature,
                    RTE_NODEJS_EXTENSION_GROUP_SIGNATURE);
  rte_sanitizer_thread_check_init_with_current_thread(
      &extension_group_bridge->thread_check);

  napi_status status = napi_wrap(env, args[0], extension_group_bridge,
                                 rte_nodejs_extension_group_finalize, NULL,
                                 &extension_group_bridge->js_ref);
  GOTO_LABEL_IF_NAPI_FAIL(error, status == napi_ok,
                          "Failed to bind JS extension group & bridge: %d",
                          status);
  // There are two references to this extension_group_bridge, one is from the JS
  // world, and the other is from the native part. The reference count created
  // from the following rte_sharedptr_create() is of JS extension group.
  extension_group_bridge->shared_ptr = rte_sharedptr_create(
      extension_group_bridge, rte_nodejs_extension_group_destroy);

  extension_group_bridge->c_extension_group = rte_extension_group_create(
      rte_string_c_str(&name), proxy_on_init, proxy_on_deinit,
      proxy_on_create_extension, proxy_on_delete_extension);
  rte_extension_group_set_me_in_target_lang(
      extension_group_bridge->c_extension_group, extension_group_bridge);
  // The reference count created from the following rte_sharedptr_inc_rc() is of
  // C extension group.
  rte_sharedptr_inc_rc(extension_group_bridge->shared_ptr);

  rte_nodejs_extension_group_attach_callbacks(env, extension_group_bridge);

  goto done;

error:
  if (extension_group_bridge) {
    rte_free(extension_group_bridge);
  }

done:
  rte_string_deinit(&name);

  return UNDEFINED(env);
}

/**
 * @brief This function would be called when the C extension group ends.
 */
static napi_value rte_nodejs_extension_group_deinit(napi_env env,
                                                    napi_callback_info info) {
  assert(env);
  assert(info);

  const size_t argc = 1;
  napi_value args[argc];  // extension_group
  if (!rte_nodejs_get_args(env, info, args, argc)) {
    assert(0 && "Should not happen");
  }

  rte_nodejs_extension_group_t* extension_group_bridge = NULL;
  napi_status status =
      napi_unwrap(env, args[0], (void**)&extension_group_bridge);
  RETURN_UNDEFINED_IF_NAPI_FAIL(
      status == napi_ok && extension_group_bridge != NULL,
      "Failed to get extension_group bridge: %d", status);
  assert(extension_group_bridge && rte_nodejs_extension_group_check_integrity(
                                       extension_group_bridge, true));

  // The C extension group is going to be destroyed, decrease the reference
  // count of the bridge to indicate that one of the ownership of the extension
  // group bridge will not be hold by C extension group.
  rte_sharedptr_dec_rc(extension_group_bridge->shared_ptr);

  rte_nodejs_extension_group_release_callbacks(env, extension_group_bridge);

  return UNDEFINED(env);
}

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

  EXPORT_FUNC(env, exports, rte_nodejs_extension_group_create);
  EXPORT_FUNC(env, exports, rte_nodejs_extension_group_deinit);

  return exports;
}
