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

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

#include "lib/alloc.h"
#include "lib/shared_ptr.h"
#include "lib/signature.h"
#include "rte_runtime/app/internal.h"
#include "rte_runtime/binding/nodejs/src/app/app.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/metadata/metadata.h"
#include "rte_runtime/sanitizer/thread_check.h"

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

typedef struct app_on_init_call_info_t {
  rte_nodejs_app_t* app_bridge;
  rte_t* rte;
  rte_metadata_info_t* manifest;
  rte_metadata_info_t* property;
  rte_nodejs_sem_t sem;
  bool js_result;
} app_on_init_call_info_t;

typedef struct app_on_close_call_info_t {
  rte_nodejs_app_t* app_bridge;
  rte_nodejs_sem_t sem;
  bool js_result;
} app_on_close_call_info_t;

UNUSED static bool rte_nodejs_app_check_integrity(rte_nodejs_app_t* self,
                                                  bool check_thread) {
  assert(self);

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

static void rte_nodejs_app_detach_callbacks(rte_nodejs_app_t* self) {
  assert(self &&
         // RTE_NOLINTNEXTLINE(thread-check)
         // thread-check: if reach here, it means JS app & C app are all end, so
         // it's thread safe here.
         rte_nodejs_app_check_integrity(self, false));

  rte_nodejs_threadsafe_function_dec_rc(self->js_on_init);
  rte_nodejs_threadsafe_function_dec_rc(self->js_on_close);
}

static void rte_nodejs_app_release_callbacks(napi_env env,
                                             rte_nodejs_app_t* self) {
  assert(env && self && rte_nodejs_app_check_integrity(self, true));

  rte_nodejs_threadsafe_function_release(env, self->js_on_init);
  rte_nodejs_threadsafe_function_release(env, self->js_on_close);
}

// Invoked when the JS app finalizes.
static void rte_nodejs_app_finalize(napi_env env, void* data,
                                    UNUSED void* hint) {
  ZF_LOGW("RTE JS app is finalized.");

  rte_nodejs_app_t* app_bridge = data;
  assert(app_bridge && rte_nodejs_app_check_integrity(app_bridge, true));

  // 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, app_bridge->js_rte_ref, &rte_ref_count);
  GOTO_LABEL_IF_NAPI_FAIL(error, status == napi_ok && rte_ref_count == 0,
                          "Failed to unref JS rte reference in app: %d",
                          status);

  // The JS extension is finalized, so delete the reference to it.
  status = napi_delete_reference(env, app_bridge->js_ref);
  GOTO_LABEL_IF_NAPI_FAIL(error, status == napi_ok,
                          "Failed to delete reference to JS app: %d", status);

error:
  app_bridge->js_ref = NULL;

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

static void invoke_app_js_on_init(napi_env env, napi_value fn,
                                  UNUSED void* context, void* data) {
  app_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->app_bridge->js_rte_ref) {
    status = napi_create_reference(env, js_rte, 1,
                                   &call_info->app_bridge->js_rte_ref);
    GOTO_LABEL_IF_NAPI_FAIL(error, status == napi_ok,
                            "Failed to create C reference to JS 'rte': %d",
                            status);
  }

  napi_value js_app = NULL;
  status =
      napi_get_reference_value(env, call_info->app_bridge->js_ref, &js_app);
  GOTO_LABEL_IF_NAPI_FAIL(error, status == napi_ok && js_app != NULL,
                          "Failed to get JS app: %d", status);

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

  goto done;

error:
  call_info->js_result = false;

  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_app_t* app, rte_t* rte,
                          rte_metadata_info_t* manifest,
                          rte_metadata_info_t* property) {
  assert(app && rte_app_check_integrity(app, true));
  assert(rte && rte_check_integrity(rte));
  assert(app->rte == rte);

  rte_nodejs_app_t* app_bridge = rte_app_get_me_in_target_lang(app);
  assert(app_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 app thread access the
         // app bridge at the same time.
         rte_nodejs_app_check_integrity(app_bridge, false));

  // If the app is default, we call default_init first.
  if (app_bridge->is_default) {
    rte_app_default_init_without_done(app, manifest, property);
  }

  app_on_init_call_info_t call_info = {
      .app_bridge = app_bridge,
      .rte = rte,
      .manifest = manifest,
      .property = property,
      .js_result = true,
  };

  PREPARE_TO_CALL_TSFN(call_info);

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

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

  if (!rc || !call_info.js_result) {
    // Failed to call JS on_init(), so that we need to call on_init_done() here
    // to let RTE runtime proceed.
    rte_on_init_done(rte, manifest, property);
  }
}

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

  rte_nodejs_app_t* app_bridge = call_info->app_bridge;
  assert(app_bridge && rte_nodejs_app_check_integrity(app_bridge, true));

  napi_value js_app = NULL;
  napi_status status =
      napi_get_reference_value(env, app_bridge->js_ref, &js_app);
  GOTO_LABEL_IF_NAPI_FAIL(error, status == napi_ok && js_app != NULL,
                          "Failed to get JS app: %d", status);

  napi_value js_rte = NULL;
  status = napi_get_reference_value(env, app_bridge->js_rte_ref, &js_rte);
  GOTO_LABEL_IF_NAPI_FAIL(error, status == napi_ok && js_rte != NULL,
                          "Failed to get JS rte: %d", status);

  napi_value result = NULL;
  status = napi_call_function(env, js_app, fn, 1, &js_rte, &result);
  GOTO_LABEL_IF_NAPI_FAIL(error, status == napi_ok,
                          "Failed to call app on_close().");

  goto done;

error:
  call_info->js_result = false;

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

static void proxy_on_close(rte_app_t* app, rte_t* rte) {
  assert(app && rte_app_check_integrity(app, true));

  rte_nodejs_app_t* app_bridge = rte_app_get_me_in_target_lang(app);
  assert(app_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 app thread access the
         // app bridge at the same time.
         rte_nodejs_app_check_integrity(app_bridge, false));

  // If the app is default, we call default_deinit first.
  if (app_bridge->is_default) {
    rte_app_default_deinit_without_done(app);
  }

  app_on_close_call_info_t call_info = {
      .app_bridge = app_bridge,
      .js_result = true,
  };

  PREPARE_TO_CALL_TSFN(call_info);

  bool rc = rte_nodejs_threadsafe_function_invoke(app_bridge->js_on_close,
                                                  &call_info);

  END_TO_CALL_TSFN(call_info, "Failed to call app on_close().");

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

  // The native app is closed, so we release the reference to the app_bridge
  // now. Even if on_close() is failed, we still release the reference.
  rte_sharedptr_dec_rc(app_bridge->shared_ptr);
}

static void rte_nodejs_app_attach_callbacks(napi_env env,
                                            rte_nodejs_app_t* app_bridge) {
  assert(app_bridge && rte_nodejs_app_check_integrity(app_bridge, true));

  napi_value js_app = NULL;
  napi_status status =
      napi_get_reference_value(env, app_bridge->js_ref, &js_app);
  ASSERT_IF_NAPI_FAIL(status == napi_ok && js_app != NULL,
                      "Failed to get JS app: %d", status);

  napi_value js_cb_on_init = rte_nodejs_get_property(env, js_app, "_onInit");
  CREATE_JS_CALLBACK(app_bridge->js_on_init, env, "app::onInit", js_cb_on_init,
                     invoke_app_js_on_init);

  napi_value js_cb_close_init =
      rte_nodejs_get_property(env, js_app, "_onClose");
  CREATE_JS_CALLBACK(app_bridge->js_on_close, env, "app::onClose",
                     js_cb_close_init, invoke_app_js_on_close);
}

static void rte_nodejs_app_destroy(rte_nodejs_app_t* self) {
  assert(self &&
         // RTE_NOLINTNEXTLINE(thread-check)
         // thread-check: if reach here, it means JS app & C app are all end, so
         // it's thread safe here.
         rte_nodejs_app_check_integrity(self, false));

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

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

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

  rte_nodejs_app_t* app_bridge = rte_malloc(sizeof(rte_nodejs_app_t));
  assert(app_bridge);

  memset(app_bridge, 0, sizeof(rte_nodejs_app_t));
  rte_signature_set(&app_bridge->signature, RTE_NODEJS_APP_SIGNATURE);
  rte_sanitizer_thread_check_init_with_current_thread(
      &app_bridge->thread_check);

  // Wraps the native bridge instance ('app_bridge') in the javascript APP
  // object (args[0]). And the returned reference ('js_ref' here) is a weak
  // reference, meaning it has a reference count of 0.
  napi_status status =
      napi_wrap(env, args[0], app_bridge, rte_nodejs_app_finalize, NULL,
                &app_bridge->js_ref);
  GOTO_LABEL_IF_NAPI_FAIL(error, status == napi_ok,
                          "Failed to bind JS app & bridge: %d", status);

  // There are two references to this app_bridge, one is from the JS world, and
  // the other is from the native part. The reference count from the following
  // rte_sharedptr_create() is of JS app.
  app_bridge->shared_ptr =
      rte_sharedptr_create(app_bridge, rte_nodejs_app_destroy);

  status = napi_get_value_bool(env, args[1], &app_bridge->is_default);
  assert(status == napi_ok);

  // The above 'js_ref' is a weak reference, and it will cause the wrapped
  // JS APP object be garbage collected at any time. So that we need to use
  // 'napi_reference_ref' to increase the reference count, and it will cause
  // the 'js_ref' to transform into a persistent (strong) reference, such
  // that the wrapped JS APP object will not be garbage collected, and remain
  // valid.
  uint32_t js_ref_count = 0;
  status = napi_reference_ref(env, app_bridge->js_ref, &js_ref_count);
  GOTO_LABEL_IF_NAPI_FAIL(error, status == napi_ok,
                          "Failed to increment JS app reference: %d", status);

  app_bridge->c_app = rte_app_create(proxy_on_init, proxy_on_close);
  rte_app_set_me_in_target_lang(app_bridge->c_app, app_bridge);
  // The reference count from the following rte_sharedptr_inc_rc() is of C app.
  rte_sharedptr_inc_rc(app_bridge->shared_ptr);

  rte_nodejs_app_attach_callbacks(env, app_bridge);

  goto done;

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

done:
  return UNDEFINED(env);
}

static void nodejs_app_thread_main(void* data) {
  rte_nodejs_app_t* app_bridge = data;
  assert(app_bridge &&
         // RTE_NOLINTNEXTLINE(thread-check)
         // thread-check: app_bridge should be used in JS main thread, and all
         // the operations of app_bridge in this function are thread-safe.
         rte_nodejs_app_check_integrity(app_bridge, false));
  assert(app_bridge == rte_app_get_me_in_target_lang(app_bridge->c_app));

  ZF_LOGV("RTE nodejs app thread starts.");

  // The latter part of this function would use 'app_bridge', so we need to
  // increase its reference counter here.
  rte_sharedptr_inc_rc(app_bridge->shared_ptr);

  rte_app_t* c_app = app_bridge->c_app;
  assert(c_app);

  // This is the app thread, so we transfer the thread ownership of the RTE app
  // to this thread.
  rte_sanitizer_thread_check_set_belonging_thread_to_current_thread(
      &c_app->thread_check);

  assert(rte_app_check_integrity(c_app, true));

  rte_app_run(c_app,
              /* Because we are in a new thread, it's unnecessary to create a
                 new one. */
              false);

  rte_app_set_me_in_target_lang(c_app, NULL);
  rte_app_destroy(c_app);

  assert(app_bridge &&
         // RTE_NOLINTNEXTLINE(thread-check)
         // thread-check: all remaining operations of app_bridge are all thread
         // safe.
         rte_nodejs_app_check_integrity(app_bridge, false));

  // We have done all the things we want to do with app_bridge, so
  // decrease its reference counter here.
  rte_sharedptr_dec_rc(app_bridge->shared_ptr);

  ZF_LOGV("RTE nodejs app thread ends.");
}

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

  ZF_LOGV("App run.");

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

  rte_nodejs_app_t* app_bridge = NULL;
  napi_status status = napi_unwrap(env, args[0], (void**)&app_bridge);
  RETURN_UNDEFINED_IF_NAPI_FAIL(status == napi_ok && app_bridge != NULL,
                                "Failed to get app bridge: %d", status);
  assert(app_bridge && rte_nodejs_app_check_integrity(app_bridge, true));

  // Run the app in another thread, so that libevent won't block libuv.
  uv_thread_t tid = 0;
  uv_thread_create(&tid, nodejs_app_thread_main, app_bridge);

  return UNDEFINED(env);
}

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

  ZF_LOGV("App stop.");

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

  rte_nodejs_app_t* app_bridge = NULL;
  napi_status status = napi_unwrap(env, args[0], (void**)&app_bridge);
  RETURN_UNDEFINED_IF_NAPI_FAIL(status == napi_ok && app_bridge != NULL,
                                "Failed to get app bridge: %d", status);
  assert(app_bridge && rte_nodejs_app_check_integrity(app_bridge, true));

  rte_app_stop(app_bridge->c_app);

  return UNDEFINED(env);
}

// TODO(Lyuge): this is not working since it blocks main thread.
static napi_value rte_nodejs_app_wait(napi_env env, napi_callback_info info) {
  assert(env && info);

  ZF_LOGV("App wait.");

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

  rte_nodejs_app_t* app_bridge = NULL;
  napi_status status = napi_unwrap(env, args[0], (void**)&app_bridge);
  RETURN_UNDEFINED_IF_NAPI_FAIL(status == napi_ok && app_bridge != NULL,
                                "Failed to get app bridge: %d", status);
  assert(app_bridge && rte_nodejs_app_check_integrity(app_bridge, true));

  rte_app_wait(app_bridge->c_app);

  return UNDEFINED(env);
}

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

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

  rte_nodejs_app_t* app_bridge = NULL;
  napi_status status = napi_unwrap(env, args[0], (void**)&app_bridge);
  RETURN_UNDEFINED_IF_NAPI_FAIL(status == napi_ok && app_bridge != NULL,
                                "Failed to get app bridge: %d", status);
  assert(app_bridge && rte_nodejs_app_check_integrity(app_bridge, true));

  rte_nodejs_app_release_callbacks(env, app_bridge);

  // 'napi_reference_unref' will decrease the reference count, and 'js_ref'
  // will transform into a wark reference if the reference count is 0, then the
  // wrapped JS app will be garbage collected.
  uint32_t js_ref_count = 0;
  status = napi_reference_unref(env, app_bridge->js_ref, &js_ref_count);
  ASSERT_IF_NAPI_FAIL(status == napi_ok && js_ref_count == 0,
                      "Failed to unref JS app: %d", status);

  return UNDEFINED(env);
}

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

  EXPORT_FUNC(env, exports, rte_nodejs_app_create);
  EXPORT_FUNC(env, exports, rte_nodejs_app_run);
  EXPORT_FUNC(env, exports, rte_nodejs_app_stop);
  EXPORT_FUNC(env, exports, rte_nodejs_app_wait);
  EXPORT_FUNC(env, exports, rte_nodejs_app_deinit);

  return exports;
}
