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

#include <assert.h>

#include "jansson.h"
#include "js_native_api.h"
#include "js_native_api_types.h"
#include "lib/alloc.h"
#include "lib/buf.h"
#include "lib/shared_ptr.h"
#include "lib/signature.h"
#include "lib/string.h"
#include "node_api.h"
#include "rte_runtime/binding/nodejs/src/common/common.h"
#include "rte_runtime/binding/nodejs/src/value/js_ref.h"
#include "rte_runtime/rte/rte.h"
#include "rte_runtime/sanitizer/thread_check.h"
#include "schema/type.h"
#include "schema/value.h"

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

static napi_ref js_value_constructor_ref = NULL;  // NOLINT

bool rte_nodejs_value_check_integrity(rte_nodejs_value_t* self,
                                      bool check_thread) {
  assert(self);

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

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

  return true;
}

static void rte_nodejs_value_finalize(napi_env env, void* data,
                                      UNUSED void* hint) {
  rte_nodejs_value_t* value_bridge = data;
  assert(value_bridge && rte_nodejs_value_check_integrity(value_bridge, true));

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

  // 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, value_bridge->js_ref);
  ASSERT_IF_NAPI_FAIL(status == napi_ok,
                      "Failed to delete reference of JS value.");

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

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

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

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

  return UNDEFINED(env);
}

static void rte_nodejs_value_destroy(void* value_bridge_) {
  rte_nodejs_value_t* value_bridge = value_bridge_;
  assert(value_bridge && rte_nodejs_value_check_integrity(value_bridge, true));

  ZF_LOGV("Destroy value bridge.");

  // When the value bridge is destroyed, it will destroy the underlying C value,
  // too.
  if (value_bridge->c_value) {
    rte_value_destroy(value_bridge->c_value);
    value_bridge->c_value = NULL;
  }

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

static rte_nodejs_value_t* rte_nodejs_value_create_from_c_value(
    napi_env env, rte_value_t* c_value) {
  assert(env);

  rte_nodejs_value_t* value_bridge = rte_malloc(sizeof(rte_nodejs_value_t));
  assert(value_bridge);

  rte_signature_set(&value_bridge->signature, RTE_NODEJS_VALUE_SIGNATURE);
  value_bridge->c_value = c_value;
  value_bridge->js_ref = NULL;

  rte_sanitizer_thread_check_init_with_current_thread(
      &value_bridge->thread_check);

  // Important! The C value doesn't contribute 1 shared-ptr reference count to
  // the value bridge.

  return value_bridge;
}

static void rte_nodejs_value_set_js_ref(napi_env env,
                                        rte_nodejs_value_t* value_bridge,
                                        napi_ref js_ref) {
  assert(env);
  assert(value_bridge && rte_nodejs_value_check_integrity(value_bridge, true));
  assert(js_ref);

  value_bridge->js_ref = js_ref;

  // Important! The JS value has full control over the entire life cycle of the
  // JS value, value bridge, and the underlying C value, so the value bridge
  // will _not_ hold a JS reference count to the JS value.

  // The initial reference count (1) indicates that the JS value references to
  // the value bridge.
  value_bridge->shared_ptr =
      rte_sharedptr_create(value_bridge, rte_nodejs_value_destroy);
}

napi_value rte_nodejs_value_wrap(napi_env env, rte_value_t* value) {
  assert(env && value);

  rte_nodejs_value_t* value_bridge =
      rte_nodejs_value_create_from_c_value(env, value);
  assert(value_bridge && rte_nodejs_value_check_integrity(value_bridge, true));

  napi_ref js_ref = NULL;
  napi_value instance = rte_nodejs_create_new_js_object_and_wrap(
      env, js_value_constructor_ref, value_bridge, rte_nodejs_value_finalize,
      &js_ref);
  assert(js_ref);

  rte_nodejs_value_set_js_ref(env, value_bridge, js_ref);

  return instance;
}

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

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

  napi_valuetype arg_type = 0;
  napi_typeof(env, args[1], &arg_type);

  napi_status status = napi_ok;

  rte_value_t* c_value = NULL;
  if (arg_type == napi_boolean) {
    bool v = false;
    status = napi_get_value_bool(env, args[1], &v);
    ASSERT_IF_NAPI_FAIL(status == napi_ok, "Failed to get JS boolean: %d",
                        status);

    c_value = rte_value_create_boolean(v);
  } else if (arg_type == napi_number) {
    double v = 0;
    status = napi_get_value_double(env, args[1], &v);
    ASSERT_IF_NAPI_FAIL(status == napi_ok, "Failed to get JS double: %d",
                        status);

    c_value = rte_value_create_float64(v);
  } else if (arg_type == napi_string) {
    rte_string_t content;
    rte_string_init(&content);

    UNUSED bool rc = rte_nodejs_get_str(env, args[1], &content);
    assert(rc);

    c_value = rte_value_create_string(rte_string_c_str(&content));

    rte_string_deinit(&content);
  } else if (arg_type == napi_object) {
    bool is_buffer = false;

    status = napi_is_buffer(env, args[1], &is_buffer);
    ASSERT_IF_NAPI_FAIL(status == napi_ok,
                        "Failed to determine JS value is a JS buffer: %d",
                        status);

    if (is_buffer) {
      // The object is a buffer.

      rte_buf_t buf = {0};
      status = napi_get_buffer_info(env, args[1], &buf.buf, &buf.buf_size);
      ASSERT_IF_NAPI_FAIL(status == napi_ok, "Failed to get JS buffer info: %d",
                          status);

      c_value = rte_value_create_buf(buf);
    } else {
      // Otherwise, remember the 'pointer' of the JS instance.

      c_value = rte_nodejs_js_ref_create(env, args[1]);
    }
  } else if (arg_type == napi_undefined) {
    // Do nothing. This path should only be called from C to JS, to create a JS
    // value without the C body.
  } else {
    ZF_LOGE("Unsupported JS value type %d", arg_type);

    status = napi_throw_error(env, "EINVAL", "Unsupported JS value type.");
    ASSERT_IF_NAPI_FAIL(status == napi_ok, "Failed to throw JS exception: %d",
                        status);

    return UNDEFINED(env);
  }

  if (c_value) {
    rte_nodejs_value_t* value_bridge =
        rte_nodejs_value_create_from_c_value(env, c_value);
    assert(value_bridge &&
           rte_nodejs_value_check_integrity(value_bridge, true));

    napi_ref js_ref = NULL;
    status = napi_wrap(env, args[0], value_bridge, rte_nodejs_value_finalize,
                       NULL, &js_ref);
    ASSERT_IF_NAPI_FAIL(status == napi_ok,
                        "Failed to bind JS value & bridge: %d", status);

    rte_nodejs_value_set_js_ref(env, value_bridge, js_ref);
  }

  return UNDEFINED(env);
}

static napi_value rte_nodejs_value_get_boolean(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_value_t* value_bridge = NULL;
  napi_status status = napi_unwrap(env, args[0], (void**)&value_bridge);
  ASSERT_IF_NAPI_FAIL(status == napi_ok && value_bridge != NULL,
                      "Failed to get value bridge: %d", status);

  assert(value_bridge && rte_nodejs_value_check_integrity(value_bridge, true));
  assert(value_bridge->c_value);

  bool result = rte_value_get_boolean(value_bridge->c_value);

  napi_value js_result = NULL;
  status = napi_get_boolean(env, result, &js_result);
  RETURN_UNDEFINED_IF_NAPI_FAIL(status == napi_ok,
                                "Failed to get JS boolean: %d", status);

  return js_result;
}

static napi_value rte_nodejs_value_get_number(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_value_t* value_bridge = NULL;
  napi_status status = napi_unwrap(env, args[0], (void**)&value_bridge);
  ASSERT_IF_NAPI_FAIL(status == napi_ok && value_bridge != NULL,
                      "Failed to get value bridge: %d", status);

  assert(value_bridge && rte_nodejs_value_check_integrity(value_bridge, true));
  assert(value_bridge->c_value);

  rte_value_t* value = value_bridge->c_value;
  assert(value && rte_value_check_integrity(value));

  napi_value js_result = NULL;

  switch (value->type) {
    case RTE_TYPE_FLOAT64: {
      double result = rte_value_get_float64(value);
      status = napi_create_double(env, result, &js_result);
      break;
    }

    case RTE_TYPE_FLOAT32: {
      double result = rte_value_get_float32(value);
      status = napi_create_double(env, result, &js_result);
      break;
    }

    case RTE_TYPE_INT32: {
      int32_t result = rte_value_get_int32(value);
      status = napi_create_int32(env, result, &js_result);
      break;
    }

    case RTE_TYPE_INT64: {
      int64_t result = rte_value_get_int64(value);
      status = napi_create_int64(env, result, &js_result);
      break;
    }

    default:
      status = napi_throw_error(
          env, "EINVAL",
          "The Number type should be int32/int64/float32/float64.");
      ASSERT_IF_NAPI_FAIL(status == napi_ok, "Failed to throw JS exception: %d",
                          status);
      return UNDEFINED(env);
  }

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

  return js_result;
}

static napi_value rte_nodejs_value_get_string(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_value_t* value_bridge = NULL;
  napi_status status = napi_unwrap(env, args[0], (void**)&value_bridge);
  ASSERT_IF_NAPI_FAIL(status == napi_ok && value_bridge != NULL,
                      "Failed to get value bridge: %d", status);

  assert(value_bridge && rte_nodejs_value_check_integrity(value_bridge, true));
  assert(value_bridge->c_value);

  rte_string_t* result = rte_value_get_string(value_bridge->c_value);

  napi_value js_result = NULL;
  status = napi_create_string_utf8(env, rte_string_c_str(result),
                                   rte_string_len(result), &js_result);
  RETURN_UNDEFINED_IF_NAPI_FAIL(status == napi_ok,
                                "Failed to create JS string: %d", status);

  return js_result;
}

static napi_value rte_nodejs_value_get_reference(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_value_t* value_bridge = NULL;
  napi_status status = napi_unwrap(env, args[0], (void**)&value_bridge);
  ASSERT_IF_NAPI_FAIL(status == napi_ok && value_bridge != NULL,
                      "Failed to get value bridge: %d", status);

  assert(value_bridge && rte_nodejs_value_check_integrity(value_bridge, true));
  assert(value_bridge->c_value);

  napi_value js_result = NULL;

  switch (value_bridge->c_value->type) {
    case RTE_TYPE_SHARED_PTR: {
      // The type of the value should 'rte_nodejs_js_ref_t'.
      rte_sharedptr_t* shared_ptr =
          rte_value_get_shared_ptr(value_bridge->c_value);
      assert(shared_ptr);

      rte_nodejs_js_ref_t* result = rte_sharedptr_get_data(shared_ptr);
      assert(result && result->js_ref);

      status = napi_get_reference_value(env, result->js_ref, &js_result);
      RETURN_UNDEFINED_IF_NAPI_FAIL(status == napi_ok && js_result != NULL,
                                    "Failed to get JS instance: %d", status);
      break;
    }

    case RTE_TYPE_OBJECT:
    case RTE_TYPE_ARRAY: {
      // According to RFC 4627, object & array are the only two valid root
      // elements of a JSON object, so we can use JSON string as an intermediate
      // representation to convert from RTE C object|array values to JavaScript
      // object|array values here.

      // TODO(Wei): Do not use JSON text as the intermediate step, convert from
      // C value to JS object/array directly.

      // Convert to JSON text first.
      json_t* value_json = rte_value_to_json(value_bridge->c_value);
      if (json_is_null(value_json)) {
        status = napi_throw_error(env, "EINVAL",
                                  "Failed to serialize Value to JSON.");
        RETURN_UNDEFINED_IF_NAPI_FAIL(
            status == napi_ok, "Failed to throw JS exception: %d", status);
      }

      // Send the resultant text string to JavaScript world.
      char* value_str = json_dumps(value_json, 0);
      status = napi_create_string_utf8(env, value_str, strlen(value_str),
                                       &js_result);
      free(value_str);
      json_decref(value_json);

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

    default:
      ZF_LOGE("Unsupported JS value content type: %d",
              value_bridge->c_value->type);

      status =
          napi_throw_error(env, "EINVAL", "Unsupported JS value content type.");
      ASSERT_IF_NAPI_FAIL(status == napi_ok, "Failed to throw JS exception: %d",
                          status);

      return UNDEFINED(env);
  }

  return js_result;
}

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

  EXPORT_FUNC(env, exports, rte_nodejs_value_register_class);
  EXPORT_FUNC(env, exports, rte_nodejs_value_create);
  EXPORT_FUNC(env, exports, rte_nodejs_value_get_boolean);
  EXPORT_FUNC(env, exports, rte_nodejs_value_get_number);
  EXPORT_FUNC(env, exports, rte_nodejs_value_get_string);
  EXPORT_FUNC(env, exports, rte_nodejs_value_get_reference);

  return exports;
}
