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

#include <assert.h>

#include "jansson.h"
#include "js_native_api.h"
#include "lib/alloc.h"
#include "lib/atomic.h"
#include "lib/json.h"
#include "lib/shared_ptr.h"
#include "lib/string.h"
#include "rte_runtime/binding/nodejs/src/common/common.h"
#include "rte_runtime/binding/nodejs/src/msg/msg.h"
#include "rte_runtime/binding/nodejs/src/value/value.h"
#include "rte_runtime/common/constant_str.h"
#include "rte_runtime/msg/cmd/cmd.h"
#include "rte_runtime/msg/cmd/custom/cmd.h"
#include "rte_runtime/msg/field/properties.h"
#include "rte_runtime/msg/msg.h"
#include "rte_runtime/rte/rte.h"
#include "schema/value.h"

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

static napi_ref js_cmd_constructor_ref = NULL;  // NOLINT

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

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

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

  return UNDEFINED(env);
}

/**
 * @brief This function would be called when the JS cmd is finalized.
 */
static void rte_nodejs_cmd_finalize(napi_env env, void* data,
                                    UNUSED void* hint) {
  rte_nodejs_cmd_t* cmd_bridge = data;
  assert(cmd_bridge && rte_nodejs_msg_check_integrity(&cmd_bridge->msg, true));

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

  rte_nodejs_msg_finalize(env, &cmd_bridge->msg);
  rte_nodejs_msg_release_ownership_from_js(&cmd_bridge->msg);
}

/**
 * @brief Wrap the C command to a newly created JS command.
 */
napi_value rte_nodejs_cmd_wrap(napi_env env, rte_sharedptr_t* cmd) {
  assert(cmd && rte_cmd_check_integrity(cmd));

  rte_nodejs_cmd_t* self = rte_malloc(sizeof(rte_nodejs_cmd_t));
  assert(self);

  rte_nodejs_msg_init_from_c_msg(&self->msg, cmd);
  rte_nodejs_msg_take_ownership_from_js(&self->msg);

  return rte_nodejs_create_new_js_object_and_wrap(env, js_cmd_constructor_ref,
                                                  self, rte_nodejs_cmd_finalize,
                                                  &self->msg.js_ref);
}

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

  rte_string_t json_str;
  rte_string_init(&json_str);

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

  napi_valuetype arg_type = napi_undefined;
  napi_status status = napi_typeof(env, args[1], &arg_type);
  ASSERT_IF_NAPI_FAIL(status == napi_ok,
                      "Failed to get type of JS instance: %d", status);

  if (arg_type != napi_undefined) {
    if (!is_string(env, args[1])) {
      status = napi_throw_error(
          env, "EINVAL",
          "The parameter of rte_nodejs_cmd_create must be a json string.");
      ASSERT_IF_NAPI_FAIL(status == napi_ok, "Failed to throw JS exception: %d",
                          status);

      goto done;
    }

    rte_nodejs_get_str(env, args[1], &json_str);

    json_error_t err = {0};
    json_t* json = json_loads(rte_string_c_str(&json_str), 0, &err);
    if (!json) {
      rte_string_t err_str;
      rte_string_init_with_value(&err_str,
                                 "Failed to load json, error at (%d,%d): %s",
                                 err.line, err.column, err.text);
      status = napi_throw_error(env, "EINVAL", rte_string_c_str(&err_str));
      ASSERT_IF_NAPI_FAIL(status == napi_ok, "Failed to throw JS exception: %d",
                          status);

      rte_string_deinit(&err_str);

      goto done;
    }

    rte_sharedptr_t* c_cmd = rte_msg_from_json(json, NULL);
    assert(c_cmd);

    json_decref(json);

    // Link the JS cmd with its C cmd.
    rte_nodejs_cmd_t* cmd_bridge = rte_malloc(sizeof(rte_nodejs_cmd_t));
    assert(cmd_bridge);

    rte_nodejs_msg_init_from_c_msg(&cmd_bridge->msg, c_cmd);
    // Decrement the reference count of c_cmd to indicate that the JS cmd takes
    // the full ownership of this c_cmd, in other words, when the JS cmd is
    // finalized, its C cmd would be destroyed, too.
    rte_sharedptr_dec_rc(c_cmd);

    rte_nodejs_msg_take_ownership_from_js(&cmd_bridge->msg);

    status = napi_wrap(env, args[0], cmd_bridge, rte_nodejs_cmd_finalize, NULL,
                       &cmd_bridge->msg.js_ref);
    ASSERT_IF_NAPI_FAIL(status == napi_ok,
                        "Failed to bind JS command & bridge: %d", status);

    // TODO(Wei): Throws an error if failed to wrap.
  }

done:
  rte_string_deinit(&json_str);

  return UNDEFINED(env);
}

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

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

  rte_nodejs_cmd_t* cmd_bridge = NULL;
  napi_status status = napi_unwrap(env, args[0], (void**)&cmd_bridge);
  ASSERT_IF_NAPI_FAIL(status == napi_ok && cmd_bridge != NULL,
                      "Failed to get cmd bridge: %d", status);
  assert(cmd_bridge && rte_nodejs_msg_check_integrity(&cmd_bridge->msg, true));

  rte_string_t* cmd_id = rte_cmd_get_cmd_id(cmd_bridge->msg.c_msg);
  assert(cmd_id);

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

  return cmd_id_js;
}

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

  rte_string_t json_str;
  rte_string_init(&json_str);

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

  rte_nodejs_cmd_t* cmd_bridge = NULL;
  napi_status status = napi_unwrap(env, args[0], (void**)&cmd_bridge);
  ASSERT_IF_NAPI_FAIL(status == napi_ok && cmd_bridge != NULL,
                      "Failed to get cmd bridge: %d", status);
  assert(cmd_bridge && rte_nodejs_msg_check_integrity(&cmd_bridge->msg, true));

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

  // Should be validated in JS, so this function should never failed, hence we
  // don't need to pass an 'json_error_t' object to this function.
  json_t* json = json_loads(rte_string_c_str(&json_str), 0, NULL);
  assert(json && rte_json_check_integrity(json));

  rte_msg_set_json(cmd_bridge->msg.c_msg, json);
  json_decref(json);

done:
  rte_string_deinit(&json_str);
  return UNDEFINED(env);
}

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

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

  rte_nodejs_cmd_t* cmd_bridge = NULL;
  napi_status status = napi_unwrap(env, args[0], (void**)&cmd_bridge);
  ASSERT_IF_NAPI_FAIL(status == napi_ok && cmd_bridge != NULL,
                      "Failed to get cmd bridge: %d", status);
  assert(cmd_bridge && rte_nodejs_msg_check_integrity(&cmd_bridge->msg, true));

  json_t* json = rte_msg_to_json(cmd_bridge->msg.c_msg);
  assert(json && rte_json_check_integrity(json));

  char* json_string = json_dumps(json, 0);
  assert(json_string);

  napi_value cmd_id_js = NULL;
  status = napi_create_string_utf8(env, json_string, strlen(json_string),
                                   &cmd_id_js);
  RETURN_UNDEFINED_IF_NAPI_FAIL(status == napi_ok && cmd_id_js != NULL,
                                "Failed to create JS string: %d", status);

  rte_free(json_string);
  json_decref(json);

  return cmd_id_js;
}

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

  EXPORT_FUNC(env, exports, rte_nodejs_cmd_register_class);
  EXPORT_FUNC(env, exports, rte_nodejs_cmd_create);
  EXPORT_FUNC(env, exports, rte_nodejs_cmd_get_id);
  EXPORT_FUNC(env, exports, rte_nodejs_cmd_set_json);
  EXPORT_FUNC(env, exports, rte_nodejs_cmd_to_json);

  return exports;
}
