// For the purpose of this test we use libuv's threading library. When deciding
// on a threading library for a new project it bears remembering that in the
// future libuv may introduce API changes which may render it non-ABI-stable,
// which, in turn, may affect the ABI stability of the project despite its use
// of N-API.
#include <uv.h>
#include <node_api.h>
#include "../../js-native-api/common.h"

#define ARRAY_LENGTH 10000
#define MAX_QUEUE_SIZE 2

static uv_thread_t uv_threads[2];
static napi_threadsafe_function ts_fn;

typedef struct {
  napi_threadsafe_function_call_mode block_on_full;
  napi_threadsafe_function_release_mode abort;
  bool start_secondary;
  napi_ref js_finalize_cb;
  uint32_t max_queue_size;
} ts_fn_hint;

static ts_fn_hint ts_info;

// Thread data to transmit to JS
static int ints[ARRAY_LENGTH];

static void secondary_thread(void* data) {
  napi_threadsafe_function ts_fn = data;

  if (napi_release_threadsafe_function(ts_fn, napi_tsfn_release) != napi_ok) {
    napi_fatal_error("secondary_thread", NAPI_AUTO_LENGTH,
        "napi_release_threadsafe_function failed", NAPI_AUTO_LENGTH);
  }
}

// Source thread producing the data
static void data_source_thread(void* data) {
  napi_threadsafe_function ts_fn = data;
  int index;
  void* hint;
  ts_fn_hint *ts_fn_info;
  napi_status status;
  bool queue_was_full = false;
  bool queue_was_closing = false;

  if (napi_get_threadsafe_function_context(ts_fn, &hint) != napi_ok) {
    napi_fatal_error("data_source_thread", NAPI_AUTO_LENGTH,
        "napi_get_threadsafe_function_context failed", NAPI_AUTO_LENGTH);
  }

  ts_fn_info = (ts_fn_hint *)hint;

  if (ts_fn_info != &ts_info) {
    napi_fatal_error("data_source_thread", NAPI_AUTO_LENGTH,
      "thread-safe function hint is not as expected", NAPI_AUTO_LENGTH);
  }

  if (ts_fn_info->start_secondary) {
    if (napi_acquire_threadsafe_function(ts_fn) != napi_ok) {
      napi_fatal_error("data_source_thread", NAPI_AUTO_LENGTH,
        "napi_acquire_threadsafe_function failed", NAPI_AUTO_LENGTH);
    }

    if (uv_thread_create(&uv_threads[1], secondary_thread, ts_fn) != 0) {
      napi_fatal_error("data_source_thread", NAPI_AUTO_LENGTH,
        "failed to start secondary thread", NAPI_AUTO_LENGTH);
    }
  }

  for (index = ARRAY_LENGTH - 1; index > -1 && !queue_was_closing; index--) {
    status = napi_call_threadsafe_function(ts_fn, &ints[index],
        ts_fn_info->block_on_full);
    if (ts_fn_info->max_queue_size == 0 && (index % 1000 == 0)) {
      // Let's make this thread really busy for 200 ms to give the main thread a
      // chance to abort.
      uint64_t start = uv_hrtime();
      for (; uv_hrtime() - start < 200000000;);
    }
    switch (status) {
      case napi_queue_full:
        queue_was_full = true;
        index++;
        // fall through

      case napi_ok:
        continue;

      case napi_closing:
        queue_was_closing = true;
        break;

      default:
        napi_fatal_error("data_source_thread", NAPI_AUTO_LENGTH,
            "napi_call_threadsafe_function failed", NAPI_AUTO_LENGTH);
    }
  }

  // Assert that the enqueuing of a value was refused at least once, if this is
  // a non-blocking test run.
  if (!ts_fn_info->block_on_full && !queue_was_full) {
    napi_fatal_error("data_source_thread", NAPI_AUTO_LENGTH,
        "queue was never full", NAPI_AUTO_LENGTH);
  }

  // Assert that the queue was marked as closing at least once, if this is an
  // aborting test run.
  if (ts_fn_info->abort == napi_tsfn_abort && !queue_was_closing) {
    napi_fatal_error("data_source_thread", NAPI_AUTO_LENGTH,
      "queue was never closing", NAPI_AUTO_LENGTH);
  }

  if (!queue_was_closing &&
      napi_release_threadsafe_function(ts_fn, napi_tsfn_release) != napi_ok) {
    napi_fatal_error("data_source_thread", NAPI_AUTO_LENGTH,
        "napi_release_threadsafe_function failed", NAPI_AUTO_LENGTH);
  }
}

// Getting the data into JS
static void call_js(napi_env env, napi_value cb, void* hint, void* data) {
  if (!(env == NULL || cb == NULL)) {
    napi_value argv, undefined;
    NODE_API_CALL_RETURN_VOID(env, napi_create_int32(env, *(int*)data, &argv));
    NODE_API_CALL_RETURN_VOID(env, napi_get_undefined(env, &undefined));
    NODE_API_CALL_RETURN_VOID(env, napi_call_function(env, undefined, cb, 1, &argv,
        NULL));
  }
}

static napi_ref alt_ref;
// Getting the data into JS with the alternative reference
static void call_ref(napi_env env, napi_value _, void* hint, void* data) {
  if (!(env == NULL || alt_ref == NULL)) {
    napi_value fn, argv, undefined;
    NODE_API_CALL_RETURN_VOID(env, napi_get_reference_value(env, alt_ref, &fn));
    NODE_API_CALL_RETURN_VOID(env, napi_create_int32(env, *(int*)data, &argv));
    NODE_API_CALL_RETURN_VOID(env, napi_get_undefined(env, &undefined));
    NODE_API_CALL_RETURN_VOID(env, napi_call_function(env, undefined, fn, 1, &argv,
        NULL));
  }
}

// Cleanup
static napi_value StopThread(napi_env env, napi_callback_info info) {
  size_t argc = 2;
  napi_value argv[2];
  NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL));
  napi_valuetype value_type;
  NODE_API_CALL(env, napi_typeof(env, argv[0], &value_type));
  NODE_API_ASSERT(env, value_type == napi_function,
      "StopThread argument is a function");
  NODE_API_ASSERT(env, (ts_fn != NULL), "Existing threadsafe function");
  NODE_API_CALL(env,
      napi_create_reference(env, argv[0], 1, &(ts_info.js_finalize_cb)));
  bool abort;
  NODE_API_CALL(env, napi_get_value_bool(env, argv[1], &abort));
  NODE_API_CALL(env,
      napi_release_threadsafe_function(ts_fn,
          abort ? napi_tsfn_abort : napi_tsfn_release));
  ts_fn = NULL;
  return NULL;
}

// Join the thread and inform JS that we're done.
static void join_the_threads(napi_env env, void *data, void *hint) {
  uv_thread_t *the_threads = data;
  ts_fn_hint *the_hint = hint;
  napi_value js_cb, undefined;

  uv_thread_join(&the_threads[0]);
  if (the_hint->start_secondary) {
    uv_thread_join(&the_threads[1]);
  }

  NODE_API_CALL_RETURN_VOID(env,
      napi_get_reference_value(env, the_hint->js_finalize_cb, &js_cb));
  NODE_API_CALL_RETURN_VOID(env, napi_get_undefined(env, &undefined));
  NODE_API_CALL_RETURN_VOID(env,
      napi_call_function(env, undefined, js_cb, 0, NULL, NULL));
  NODE_API_CALL_RETURN_VOID(env, napi_delete_reference(env,
      the_hint->js_finalize_cb));
  if (alt_ref != NULL) {
    NODE_API_CALL_RETURN_VOID(env, napi_delete_reference(env, alt_ref));
    alt_ref = NULL;
  }
}

static napi_value StartThreadInternal(napi_env env,
                                      napi_callback_info info,
                                      napi_threadsafe_function_call_js cb,
                                      bool block_on_full,
                                      bool alt_ref_js_cb) {

  size_t argc = 4;
  napi_value argv[4];

  NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL));
  if (alt_ref_js_cb) {
    NODE_API_CALL(env, napi_create_reference(env, argv[0], 1, &alt_ref));
    argv[0] = NULL;
  }

  ts_info.block_on_full =
      (block_on_full ? napi_tsfn_blocking : napi_tsfn_nonblocking);

  NODE_API_ASSERT(env, (ts_fn == NULL), "Existing thread-safe function");
  napi_value async_name;
  NODE_API_CALL(env, napi_create_string_utf8(env,
      "N-API Thread-safe Function Test", NAPI_AUTO_LENGTH, &async_name));
  NODE_API_CALL(env,
      napi_get_value_uint32(env, argv[3], &ts_info.max_queue_size));
  NODE_API_CALL(env, napi_create_threadsafe_function(env,
                                                     argv[0],
                                                     NULL,
                                                     async_name,
                                                     ts_info.max_queue_size,
                                                     2,
                                                     uv_threads,
                                                     join_the_threads,
                                                     &ts_info,
                                                     cb,
                                                     &ts_fn));
  bool abort;
  NODE_API_CALL(env, napi_get_value_bool(env, argv[1], &abort));
  ts_info.abort = abort ? napi_tsfn_abort : napi_tsfn_release;
  NODE_API_CALL(env,
      napi_get_value_bool(env, argv[2], &(ts_info.start_secondary)));

  NODE_API_ASSERT(env,
      (uv_thread_create(&uv_threads[0], data_source_thread, ts_fn) == 0),
      "Thread creation");

  return NULL;
}

static napi_value Unref(napi_env env, napi_callback_info info) {
  NODE_API_ASSERT(env, ts_fn != NULL, "No existing thread-safe function");
  NODE_API_CALL(env, napi_unref_threadsafe_function(env, ts_fn));
  return NULL;
}

static napi_value Release(napi_env env, napi_callback_info info) {
  NODE_API_ASSERT(env, ts_fn != NULL, "No existing thread-safe function");
  NODE_API_CALL(env, napi_release_threadsafe_function(ts_fn, napi_tsfn_release));
  return NULL;
}

// Startup
static napi_value StartThread(napi_env env, napi_callback_info info) {
  return StartThreadInternal(env, info, call_js,
    /** block_on_full */true, /** alt_ref_js_cb */false);
}

static napi_value StartThreadNonblocking(napi_env env,
                                         napi_callback_info info) {
  return StartThreadInternal(env, info, call_js,
    /** block_on_full */false, /** alt_ref_js_cb */false);
}

static napi_value StartThreadNoNative(napi_env env, napi_callback_info info) {
  return StartThreadInternal(env, info, NULL,
    /** block_on_full */true, /** alt_ref_js_cb */false);
}

static napi_value StartThreadNoJsFunc(napi_env env, napi_callback_info info) {
  return StartThreadInternal(env, info, call_ref,
    /** block_on_full */true, /** alt_ref_js_cb */true);
}

// Testing calling into JavaScript
static void ThreadSafeFunctionFinalize(napi_env env,
                              void* finalize_data,
                              void* finalize_hint) {
  napi_ref js_func_ref = (napi_ref) finalize_data;
  napi_value js_func;
  napi_value recv;
  NODE_API_CALL_RETURN_VOID(env, napi_get_reference_value(env, js_func_ref, &js_func));
  NODE_API_CALL_RETURN_VOID(env, napi_get_global(env, &recv));
  NODE_API_CALL_RETURN_VOID(env, napi_call_function(env, recv, js_func, 0, NULL, NULL));
  NODE_API_CALL_RETURN_VOID(env, napi_delete_reference(env, js_func_ref));
}

// Testing calling into JavaScript
static napi_value CallIntoModule(napi_env env, napi_callback_info info) {
  size_t argc = 4;
  napi_value argv[4];
  NODE_API_CALL(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL));

  napi_ref finalize_func;
  NODE_API_CALL(env, napi_create_reference(env, argv[3], 1, &finalize_func));

  napi_threadsafe_function tsfn;
  NODE_API_CALL(env, napi_create_threadsafe_function(env, argv[0], argv[1], argv[2], 0, 1, finalize_func, ThreadSafeFunctionFinalize, NULL, NULL, &tsfn));
  NODE_API_CALL(env, napi_call_threadsafe_function(tsfn, NULL, napi_tsfn_blocking));
  NODE_API_CALL(env, napi_release_threadsafe_function(tsfn, napi_tsfn_release));
  return NULL;
}

// Module init
static napi_value Init(napi_env env, napi_value exports) {
  size_t index;
  for (index = 0; index < ARRAY_LENGTH; index++) {
    ints[index] = index;
  }
  napi_value js_array_length, js_max_queue_size;
  napi_create_uint32(env, ARRAY_LENGTH, &js_array_length);
  napi_create_uint32(env, MAX_QUEUE_SIZE, &js_max_queue_size);

  napi_property_descriptor properties[] = {
    {
      "ARRAY_LENGTH",
      NULL,
      NULL,
      NULL,
      NULL,
      js_array_length,
      napi_enumerable,
      NULL
    },
    {
      "MAX_QUEUE_SIZE",
      NULL,
      NULL,
      NULL,
      NULL,
      js_max_queue_size,
      napi_enumerable,
      NULL
    },
    DECLARE_NODE_API_PROPERTY("StartThread", StartThread),
    DECLARE_NODE_API_PROPERTY("StartThreadNoNative", StartThreadNoNative),
    DECLARE_NODE_API_PROPERTY("StartThreadNonblocking", StartThreadNonblocking),
    DECLARE_NODE_API_PROPERTY("StartThreadNoJsFunc", StartThreadNoJsFunc),
    DECLARE_NODE_API_PROPERTY("StopThread", StopThread),
    DECLARE_NODE_API_PROPERTY("Unref", Unref),
    DECLARE_NODE_API_PROPERTY("Release", Release),
    DECLARE_NODE_API_PROPERTY("CallIntoModule", CallIntoModule),
  };

  NODE_API_CALL(env, napi_define_properties(env, exports,
    sizeof(properties)/sizeof(properties[0]), properties));

  return exports;
}
NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)