#include <math.h>
#include <node_api.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>

#define MAX_SAFE_INTEGER_AS_DOUBLE 9007199254740991.0
#define MAX_SAFE_INTEGER_AS_UINT64 9007199254740991ULL

static void nop_finalize(napi_env env, void* finalize_data, void* finalize_hint) {
  (void)env;
  (void)finalize_data;
  (void)finalize_hint;
}

static napi_value throw_type_error(napi_env env, const char* code, const char* message) {
  napi_throw_type_error(env, code, message);
  return NULL;
}

static napi_value throw_range_error(napi_env env, const char* code, const char* message) {
  napi_throw_range_error(env, code, message);
  return NULL;
}

static bool get_size_t_arg(napi_env env, napi_value value, const char* name, size_t* result) {
  double number_value = 0;
  char message[128];

  if (napi_get_value_double(env, value, &number_value) != napi_ok) {
    snprintf(message, sizeof(message), "%s must be a number", name);
    throw_type_error(env, "ERR_INVALID_ARG_TYPE", message);
    return false;
  }

  if (!isfinite(number_value) || number_value < 0 || number_value > (double)SIZE_MAX) {
    snprintf(message, sizeof(message), "%s is out of range", name);
    throw_range_error(env, "ERR_OUT_OF_RANGE", message);
    return false;
  }

  if (floor(number_value) != number_value) {
    snprintf(message, sizeof(message), "%s must be an integer", name);
    throw_range_error(env, "ERR_OUT_OF_RANGE", message);
    return false;
  }

  *result = (size_t)number_value;
  return true;
}

static bool get_optional_size_t_arg(napi_env env, napi_value value, const char* name, size_t* result) {
  napi_valuetype value_type;
  if (napi_typeof(env, value, &value_type) != napi_ok) {
    napi_throw_error(env, NULL, "Failed to inspect argument type");
    return false;
  }

  if (value_type == napi_undefined) {
    *result = 0;
    return true;
  }

  return get_size_t_arg(env, value, name, result);
}

static bool get_max_byte_count_arg(napi_env env, napi_value value, const char* name, int64_t* result) {
  double number_value = 0;
  char message[160];

  if (napi_get_value_double(env, value, &number_value) != napi_ok) {
    snprintf(message, sizeof(message), "%s must be a number", name);
    throw_type_error(env, "ERR_INVALID_ARG_TYPE", message);
    return false;
  }

  if (!isfinite(number_value) || number_value < -1 || number_value > MAX_SAFE_INTEGER_AS_DOUBLE) {
    snprintf(message, sizeof(message), "%s must be -1 or a non-negative safe integer", name);
    throw_range_error(env, "ERR_OUT_OF_RANGE", message);
    return false;
  }

  if (floor(number_value) != number_value) {
    snprintf(message, sizeof(message), "%s must be an integer", name);
    throw_range_error(env, "ERR_OUT_OF_RANGE", message);
    return false;
  }

  *result = (int64_t)number_value;
  return true;
}

static bool get_pointer_number_arg(napi_env env, napi_value value, const char* name, uint64_t* result) {
  double number_value = 0;
  char message[160];

  if (napi_get_value_double(env, value, &number_value) != napi_ok) {
    snprintf(message, sizeof(message), "%s must be a number", name);
    throw_type_error(env, "ERR_INVALID_ARG_TYPE", message);
    return false;
  }

  if (!isfinite(number_value) || number_value < 0 || number_value > MAX_SAFE_INTEGER_AS_DOUBLE) {
    snprintf(message, sizeof(message), "%s must be a non-negative safe integer", name);
    throw_range_error(env, "ERR_OUT_OF_RANGE", message);
    return false;
  }

  if (floor(number_value) != number_value) {
    snprintf(message, sizeof(message), "%s must be an integer", name);
    throw_range_error(env, "ERR_OUT_OF_RANGE", message);
    return false;
  }

  *result = (uint64_t)number_value;
  return true;
}

static bool get_pointer_bigint_arg(napi_env env, napi_value value, const char* name, uint64_t* result) {
  bool lossless = false;
  char message[128];

  if (napi_get_value_bigint_uint64(env, value, result, &lossless) != napi_ok) {
    snprintf(message, sizeof(message), "%s must be a bigint", name);
    throw_type_error(env, "ERR_INVALID_ARG_TYPE", message);
    return false;
  }

  if (!lossless) {
    snprintf(message, sizeof(message), "%s bigint is out of range", name);
    throw_range_error(env, "ERR_OUT_OF_RANGE", message);
    return false;
  }

  return true;
}

static bool get_pointer_arg(napi_env env, napi_value value, const char* name, uint64_t* result) {
  napi_valuetype value_type;
  char message[128];

  if (napi_typeof(env, value, &value_type) != napi_ok) {
    napi_throw_error(env, NULL, "Failed to inspect argument type");
    return false;
  }

  if (value_type == napi_number) {
    return get_pointer_number_arg(env, value, name, result);
  }

  if (value_type == napi_bigint) {
    return get_pointer_bigint_arg(env, value, name, result);
  }

  snprintf(message, sizeof(message), "%s must be a number or bigint", name);
  throw_type_error(env, "ERR_INVALID_ARG_TYPE", message);
  return false;
}

static bool create_int64_result(napi_env env, int64_t value, napi_value* result) {
  if (napi_create_int64(env, value, result) != napi_ok) {
    napi_throw_error(env, NULL, "Failed to create integer result");
    return false;
  }

  return true;
}

static bool get_buffer_data(napi_env env, napi_value value, uint8_t** data) {
  bool is_buffer = false;
  if (napi_is_buffer(env, value, &is_buffer) != napi_ok) {
    napi_throw_error(env, NULL, "Failed to inspect Buffer argument");
    return false;
  }

  if (is_buffer) {
    size_t byte_length = 0;
    if (napi_get_buffer_info(env, value, (void**)data, &byte_length) != napi_ok) {
      napi_throw_error(env, NULL, "Failed to read Buffer argument");
      return false;
    }
    return true;
  }

  bool is_typed_array = false;
  if (napi_is_typedarray(env, value, &is_typed_array) != napi_ok) {
    napi_throw_error(env, NULL, "Failed to inspect TypedArray argument");
    return false;
  }

  if (is_typed_array) {
    napi_typedarray_type array_type;
    size_t element_length = 0;
    napi_value array_buffer;
    size_t byte_offset = 0;

    if (napi_get_typedarray_info(
          env,
          value,
          &array_type,
          &element_length,
          (void**)data,
          &array_buffer,
          &byte_offset
        ) != napi_ok) {
      napi_throw_error(env, NULL, "Failed to read TypedArray argument");
      return false;
    }

    (void)array_type;
    (void)element_length;
    (void)array_buffer;
    (void)byte_offset;
    return true;
  }

  bool is_dataview = false;
  if (napi_is_dataview(env, value, &is_dataview) != napi_ok) {
    napi_throw_error(env, NULL, "Failed to inspect DataView argument");
    return false;
  }

  if (is_dataview) {
    size_t byte_length = 0;
    napi_value array_buffer;
    size_t byte_offset = 0;

    if (napi_get_dataview_info(env, value, &byte_length, (void**)data, &array_buffer, &byte_offset) != napi_ok) {
      napi_throw_error(env, NULL, "Failed to read DataView argument");
      return false;
    }

    (void)byte_length;
    (void)array_buffer;
    (void)byte_offset;
    return true;
  }

  bool is_array_buffer = false;
  if (napi_is_arraybuffer(env, value, &is_array_buffer) != napi_ok) {
    napi_throw_error(env, NULL, "Failed to inspect ArrayBuffer argument");
    return false;
  }

  if (is_array_buffer) {
    size_t byte_length = 0;
    if (napi_get_arraybuffer_info(env, value, (void**)data, &byte_length) != napi_ok) {
      napi_throw_error(env, NULL, "Failed to read ArrayBuffer argument");
      return false;
    }
    return true;
  }

  throw_type_error(env, "ERR_INVALID_ARG_TYPE", "buf must be an ArrayBuffer or ArrayBufferView");
  return false;
}

static napi_value unsafe_pointer_common(napi_env env, napi_callback_info info, bool as_bigint) {
  size_t argc = 1;
  napi_value argv[1];
  napi_value result;
  uint8_t* data = NULL;
  uint64_t pointer = 0;

  if (napi_get_cb_info(env, info, &argc, argv, NULL, NULL) != napi_ok) {
    napi_throw_error(env, NULL, "Failed to read arguments");
    return NULL;
  }

  if (argc < 1) {
    return throw_type_error(env, "ERR_MISSING_ARGS", "buf is required");
  }

  if (!get_buffer_data(env, argv[0], &data)) {
    return NULL;
  }

  pointer = (uint64_t)(uintptr_t)data;

  if (as_bigint) {
    if (napi_create_bigint_uint64(env, pointer, &result) != napi_ok) {
      napi_throw_error(env, NULL, "Failed to create pointer result");
      return NULL;
    }
  } else if (napi_create_double(env, (double)pointer, &result) != napi_ok) {
    napi_throw_error(env, NULL, "Failed to create pointer result");
    return NULL;
  }

  return result;
}

static napi_value unsafePointerOf(napi_env env, napi_callback_info info) {
  return unsafe_pointer_common(env, info, false);
}

static napi_value unsafeBigIntPointerOf(napi_env env, napi_callback_info info) {
  return unsafe_pointer_common(env, info, true);
}

static napi_value unsafeArrayBufferAt(napi_env env, napi_callback_info info) {
  size_t argc = 3;
  napi_value argv[3];
  uint64_t pointer = 0;
  size_t offset = 0;
  size_t byte_length = 0;
  napi_value result;

  if (napi_get_cb_info(env, info, &argc, argv, NULL, NULL) != napi_ok) {
    napi_throw_error(env, NULL, "Failed to read arguments");
    return NULL;
  }

  if (argc < 3) {
    return throw_type_error(env, "ERR_MISSING_ARGS", "ptr, offset, and byteLength are required");
  }

  if (!get_pointer_arg(env, argv[0], "ptr", &pointer)) {
    return NULL;
  }

  if (!get_optional_size_t_arg(env, argv[1], "offset", &offset)) {
    return NULL;
  }

  if (!get_size_t_arg(env, argv[2], "byteLength", &byte_length)) {
    return NULL;
  }

  if ((uint64_t)offset > UINT64_MAX - pointer) {
    return throw_range_error(env, "ERR_OUT_OF_RANGE", "ptr + offset is out of range");
  }

  if (napi_create_external_arraybuffer(
        env,
        (uint8_t*)(uintptr_t)(pointer + (uint64_t)offset),
        byte_length,
        nop_finalize,
        NULL,
        &result
      ) != napi_ok) {
    napi_throw_error(env, NULL, "Failed to create external ArrayBuffer");
    return NULL;
  }

  return result;
}

static napi_value unsafeCountNonNullBytes(napi_env env, napi_callback_info info) {
  size_t argc = 2;
  napi_value argv[2];
  uint64_t pointer = 0;
  int64_t max_bytes = 0;
  napi_value result;
  const uint8_t* data = NULL;

  if (napi_get_cb_info(env, info, &argc, argv, NULL, NULL) != napi_ok) {
    napi_throw_error(env, NULL, "Failed to read arguments");
    return NULL;
  }

  if (argc < 2) {
    return throw_type_error(env, "ERR_MISSING_ARGS", "ptr and maxBytes are required");
  }

  if (!get_pointer_arg(env, argv[0], "ptr", &pointer)) {
    return NULL;
  }

  if (!get_max_byte_count_arg(env, argv[1], "maxBytes", &max_bytes)) {
    return NULL;
  }

  if (max_bytes == 0) {
    if (!create_int64_result(env, -1, &result)) {
      return NULL;
    }

    return result;
  }

  data = (const uint8_t*)(uintptr_t)pointer;

  if (max_bytes < 0) {
    uint64_t count = 0;

    while (data[count] != 0) {
      if (count == MAX_SAFE_INTEGER_AS_UINT64) {
        return throw_range_error(env, "ERR_OUT_OF_RANGE", "count exceeds number range");
      }

      count++;
    }

    if (!create_int64_result(env, (int64_t)count, &result)) {
      return NULL;
    }

    return result;
  }

  for (int64_t count = 0; count < max_bytes; count++) {
    if (data[count] == 0) {
      if (!create_int64_result(env, count, &result)) {
        return NULL;
      }

      return result;
    }
  }

  if (!create_int64_result(env, -1, &result)) {
    return NULL;
  }

  return result;
}

static napi_value init(napi_env env, napi_value exports) {
  napi_property_descriptor descriptors[] = {
    { "unsafePointerOf", NULL, unsafePointerOf, NULL, NULL, NULL, napi_default, NULL },
    { "unsafeBigIntPointerOf", NULL, unsafeBigIntPointerOf, NULL, NULL, NULL, napi_default, NULL },
    { "unsafeArrayBufferAt", NULL, unsafeArrayBufferAt, NULL, NULL, NULL, napi_default, NULL },
    { "unsafeCountNonNullBytes", NULL, unsafeCountNonNullBytes, NULL, NULL, NULL, napi_default, NULL },
  };

  if (napi_define_properties(env, exports, sizeof(descriptors) / sizeof(descriptors[0]), descriptors) != napi_ok) {
    napi_throw_error(env, NULL, "Failed to define exports");
    return NULL;
  }

  return exports;
}

NAPI_MODULE(NODE_GYP_MODULE_NAME, init)
