#include <stdlib.h>
#include <string.h>
#include <errno.h>
#include <unordered_map>

#include "ref-napi.h"

#ifdef _WIN32
  #define __alignof__ __alignof
  #define snprintf(buf, bufSize, format, arg) _snprintf_s(buf, bufSize, _TRUNCATE, format, arg)
  #define strtoll _strtoi64
  #define strtoull _strtoui64
  #define PRId64 "lld"
  #define PRIu64 "llu"
#else
  #ifndef __STDC_FORMAT_MACROS
    #define __STDC_FORMAT_MACROS
  #endif
  #include <inttypes.h>
#endif


using namespace Napi;

namespace {

#if !defined(NAPI_VERSION) || NAPI_VERSION < 6
napi_status napix_set_instance_data(
    napi_env env, void* data, napi_finalize finalize_cb, void* finalize_hint) {
  typedef napi_status (*napi_set_instance_data_fn)(
      napi_env env, void* data, napi_finalize finalize_cb, void* finalize_hint);
  static const napi_set_instance_data_fn napi_set_instance_data__ =
      (napi_set_instance_data_fn)
          get_symbol_from_current_process("napi_set_instance_data");

  if (napi_set_instance_data__ == nullptr)
    return napi_generic_failure;
  return napi_set_instance_data__(env, data, finalize_cb, finalize_hint);
}

napi_status napix_get_instance_data(
    napi_env env, void** data) {
  typedef napi_status (*napi_get_instance_data_fn)(
      napi_env env, void** data);
  static const napi_get_instance_data_fn napi_get_instance_data__ =
      (napi_get_instance_data_fn)
          get_symbol_from_current_process("napi_get_instance_data");

  *data = nullptr;
  if (napi_get_instance_data__ == nullptr)
    return napi_generic_failure;
  return napi_get_instance_data__(env, data);
}
#else // NAPI_VERSION >= 6
napi_status napix_set_instance_data(
    napi_env env, void* data, napi_finalize finalize_cb, void* finalize_hint) {
  return napi_set_instance_data(env, data, finalize_cb, finalize_hint);
}

napi_status napix_get_instance_data(
    napi_env env, void** data) {
  return napi_get_instance_data(env, data);
}
#endif

// used by the Int64 functions to determine whether to return a Number
// or String based on whether or not a Number will lose precision.
// http://stackoverflow.com/q/307179/376773
#define JS_MAX_INT +9007199254740992LL
#define JS_MIN_INT -9007199254740992LL

// mirrors deps/v8/src/objects.h.
// we could use `node::Buffer::kMaxLength`, but it's not defined on node v0.6.x
static const size_t kMaxLength = 0x3fffffff;

enum ArrayBufferMode {
  AB_CREATED_BY_REF,
  AB_PASSED_TO_REF
};

// Since Node.js v14.0.0, we have to keep a global list of all ArrayBuffer
// instances that we work with, in order not to create any duplicates.
// Luckily, N-API instance data is available on v14.x and above.
class InstanceData final : public RefNapi::Instance {
 public:
  InstanceData(Env env) : env(env) {}

  struct ArrayBufferEntry {
    Reference<ArrayBuffer> ab;
    size_t finalizer_count;
  };

  Env env;
  std::unordered_map<char*, ArrayBufferEntry> pointer_to_orig_buffer;
  FunctionReference buffer_from;

  void RegisterArrayBuffer(napi_value val) override {
    ArrayBuffer buf(env, val);
    RegisterArrayBuffer(buf, AB_PASSED_TO_REF);
  }

  inline void RegisterArrayBuffer(ArrayBuffer buf, ArrayBufferMode mode) {
    char* ptr = static_cast<char*>(buf.Data());
    if (ptr == nullptr) return;

    auto it = pointer_to_orig_buffer.find(ptr);
    if (it != pointer_to_orig_buffer.end()) {
      if (!it->second.ab.Value().IsEmpty()) {
        // Already have a valid entry, nothing to do.
        return;
      }
      it->second.ab.Reset(buf, 0);
      it->second.finalizer_count++;
    } else {
      pointer_to_orig_buffer.emplace(ptr, ArrayBufferEntry {
        Reference<ArrayBuffer>::New(buf, 0),
        1
      });
    }

    // If AB_CREATED_BY_REF, then another finalizer has been added before this
    // as a "real" backing store finalizer.
    if (mode != AB_CREATED_BY_REF) {
      buf.AddFinalizer([this](Env env, char* ptr) {
        UnregisterArrayBuffer(ptr);
      }, ptr);
    }
  }

  inline void UnregisterArrayBuffer(char* ptr) {
    auto it = pointer_to_orig_buffer.find(ptr);
    if (--it->second.finalizer_count == 0)
      pointer_to_orig_buffer.erase(it);
  }

  inline ArrayBuffer LookupOrCreateArrayBuffer(char* ptr, size_t length) {
    assert(ptr != nullptr);
    ArrayBuffer ab;
    auto it = pointer_to_orig_buffer.find(ptr);
    if (it != pointer_to_orig_buffer.end())
      ab = it->second.ab.Value();

    if (ab.IsEmpty()) {
      length = std::max<size_t>(length, kMaxLength);
      ab = Buffer<char>::New(env, ptr, length, [this](Env env, char* ptr) {
        UnregisterArrayBuffer(ptr);
      }).ArrayBuffer();
      RegisterArrayBuffer(ab, AB_CREATED_BY_REF);
    }
    return ab;
  }

  napi_value WrapPointer(char* ptr, size_t length) override;
  char* GetBufferData(napi_value val) override;

  static InstanceData* Get(Env env) {
    void* d = nullptr;
    if (napix_get_instance_data(env, &d) == napi_ok)
      return static_cast<InstanceData*>(d);
    return nullptr;
  }
};

/**
 * Converts an arbitrary pointer to a node Buffer with specified length
 */

Value WrapPointer(Env env, char* ptr, size_t length) {
  if (ptr == nullptr)
    length = 0;

  InstanceData* data;
  if (ptr != nullptr && (data = InstanceData::Get(env)) != nullptr) {
    ArrayBuffer ab = data->LookupOrCreateArrayBuffer(ptr, length);
    assert(!ab.IsEmpty());
    return data->buffer_from.Call({
      ab, Number::New(env, 0), Number::New(env, length)
    });
  }

  return Buffer<char>::New(env, ptr, length, [](Env,char*){});
}

char* GetBufferData(Value val) {
  Buffer<char> buf = val.As<Buffer<char>>();
  InstanceData* data = InstanceData::Get(val.Env());
  if (data != nullptr)
    data->RegisterArrayBuffer(buf.ArrayBuffer());
  return buf.Data();
}

napi_value InstanceData::WrapPointer(char* ptr, size_t length) {
  return ::WrapPointer(env, ptr, length);
}

char* InstanceData::GetBufferData(napi_value val) {
  return ::GetBufferData(Value(env, val));
}

char* AddressForArgs(const CallbackInfo& args, size_t offset_index = 1) {
  Value buf = args[0];
  if (!buf.IsBuffer()) {
    throw TypeError::New(args.Env(), "Buffer instance expected");
  }

  int64_t offset = args[offset_index].ToNumber();
  return GetBufferData(buf) + offset;
}

/**
 * Returns the pointer address as a Number of the given Buffer instance.
 * It's recommended to use `hexAddress()` in most cases instead of this function.
 *
 * WARNING: a JavaScript Number cannot precisely store a full 64-bit memory
 * address, so there's a possibility of an inaccurate value being returned
 * on 64-bit systems.
 *
 * args[0] - Buffer - the Buffer instance get the memory address of
 * args[1] - Number - optional (0) - the offset of the Buffer start at
 */

Value Address (const CallbackInfo& args) {
  char* ptr = AddressForArgs(args);
  uintptr_t intptr = reinterpret_cast<uintptr_t>(ptr);

  return Number::New(args.Env(), static_cast<double>(intptr));
}

/**
 * Returns the pointer address as a hexadecimal String. This function
 * is safe to use for displaying memory addresses, as compared to the
 * `address()` function which could overflow since it returns a Number.
 *
 * args[0] - Buffer - the Buffer instance get the memory address of
 * args[1] - Number - optional (0) - the offset of the Buffer start at
 */

Value HexAddress(const CallbackInfo& args) {
  char* ptr = AddressForArgs(args);
  char strbuf[30]; /* should be plenty... */
  snprintf(strbuf, 30, "%p", ptr);

  if (strbuf[0] == '0' && strbuf[1] == 'x') {
    /* strip the leading "0x" from the address */
    ptr = strbuf + 2;
  } else {
    ptr = strbuf;
  }

  return String::New(args.Env(), ptr);
}

/**
 * Returns "true" if the given Buffer points to nullptr, "false" otherwise.
 *
 * args[0] - Buffer - the Buffer instance to check for nullptr
 * args[1] - Number - optional (0) - the offset of the Buffer start at
 */

Value IsNull(const CallbackInfo& args) {
  char* ptr = AddressForArgs(args);
  return Boolean::New(args.Env(), ptr == nullptr);
}

/**
 * Retreives a JS Object instance that was previously stored in
 * the given Buffer instance at the given offset.
 *
 * args[0] - Buffer - the "buf" Buffer instance to read from
 * args[1] - Number - the offset from the "buf" buffer's address to read from
 */

Value ReadObject(const CallbackInfo& args) {
  char* ptr = AddressForArgs(args);

  if (ptr == nullptr) {
    throw Error::New(args.Env(), "readObject: Cannot read from nullptr pointer");
  }

  Reference<Object>* rptr = reinterpret_cast<Reference<Object>*>(ptr);
  return rptr->Value();
}

/**
 * Writes a weak reference to given Object to the given Buffer
 * instance and offset.
 *
 * args[0] - Buffer - the "buf" Buffer instance to write to
 * args[1] - Number - the offset from the "buf" buffer's address to write to
 * args[2] - Object - the "obj" Object which will have a new Persistent reference
 *                    created for the obj, whose memory address will be written.
 */

void WriteObject(const CallbackInfo& args) {
  Env env = args.Env();
  char* ptr = AddressForArgs(args);

  if (ptr == nullptr) {
    throw Error::New(env, "readObject: Cannot write to nullptr pointer");
  }

  Reference<Object>* rptr = reinterpret_cast<Reference<Object>*>(ptr);
  if (args[2].IsObject()) {
    Object val = args[2].As<Object>();
    *rptr = std::move(Reference<Object>::New(val));
  } else if (args[2].IsNull()) {
    rptr->Reset();
  } else {
    throw TypeError::New(env, "WriteObject's 3rd argument needs to be an object");
  }
}

/**
 * Reads the memory address of the given "buf" pointer Buffer at the specified
 * offset, and returns a new SlowBuffer instance from the memory address stored.
 *
 * args[0] - Buffer - the "buf" Buffer instance to read from
 * args[1] - Number - the offset from the "buf" buffer's address to read from
 * args[2] - Number - the length in bytes of the returned SlowBuffer instance
 */

Value ReadPointer(const CallbackInfo& args) {
  Env env = args.Env();
  char* ptr = AddressForArgs(args);

  if (ptr == nullptr) {
    throw Error::New(env, "readPointer: Cannot read from nullptr pointer");
  }

  int64_t size = args[2].ToNumber();

  char* val = *reinterpret_cast<char**>(ptr);
  return WrapPointer(env, val, size);
}

/**
 * Writes the memory address of the "input" buffer (and optional offset) to the
 * specified "buf" buffer and offset. Essentially making "buf" hold a reference
 * to the "input" Buffer.
 *
 * args[0] - Buffer - the "buf" Buffer instance to write to
 * args[1] - Number - the offset from the "buf" buffer's address to write to
 * args[2] - Buffer - the "input" Buffer whose memory address will be written
 */

void WritePointer(const CallbackInfo& args) {
  Env env = args.Env();
  char* ptr = AddressForArgs(args);
  Value input = args[2];

  if (!input.IsNull() && !input.IsBuffer()) {
    throw TypeError::New(env, "writePointer: Buffer instance expected as third argument");
  }

  if (input.IsNull()) {
    *reinterpret_cast<char**>(ptr) = nullptr;
  } else {
    if ((args.Length() == 4) && (args[3].As<Boolean>() == true)) {
      // create a node-api reference and finalizer to ensure that
      // the buffer whoes pointer is written can only be
      // collected after the finalizers for the buffer
      // to which the pointer was written have already run
      Reference<Value>* ref = new Reference<Value>;
      *ref = Persistent(args[2]);
      args[0].As<Object>().AddFinalizer([](Env env, Reference<Value>* ref) {
        delete ref;
      }, ref);
    }

    char* input_ptr = GetBufferData(input);
    *reinterpret_cast<char**>(ptr) = input_ptr;
  }
}

/**
 * Reads a machine-endian int64_t from the given Buffer at the given offset.
 *
 * args[0] - Buffer - the "buf" Buffer instance to read from
 * args[1] - Number - the offset from the "buf" buffer's address to read from
 */

Value ReadInt64(const CallbackInfo& args) {
  Env env = args.Env();
  char* ptr = AddressForArgs(args);

  if (ptr == nullptr) {
    throw TypeError::New(env, "readInt64: Cannot read from nullptr pointer");
  }

  int64_t val = *reinterpret_cast<int64_t*>(ptr);

  if (val < JS_MIN_INT || val > JS_MAX_INT) {
    char strbuf[128];
    snprintf(strbuf, 128, "%" PRId64, val);
    return String::New(env, strbuf);
  } else {
    return Number::New(env, val);
  }
}

/**
 * Writes the input Number/String int64 value as a machine-endian int64_t to
 * the given Buffer at the given offset.
 *
 * args[0] - Buffer - the "buf" Buffer instance to write to
 * args[1] - Number - the offset from the "buf" buffer's address to write to
 * args[2] - String/Number - the "input" String or Number which will be written
 */

void WriteInt64(const CallbackInfo& args) {
  Env env = args.Env();
  char* ptr = AddressForArgs(args);

  Value in = args[2];
  int64_t val;
  if (in.IsNumber()) {
    val = in.As<Number>();
  } else if (in.IsString()) {
    char* endptr;
    char* str;
    int base = 0;
    std::string _str = in.As<String>();
    str = &_str[0];

    errno = 0;     /* To distinguish success/failure after call */
    val = strtoll(str, &endptr, base);

    if (endptr == str) {
      throw TypeError::New(env, "writeInt64: no digits we found in input String");
    } else  if (errno == ERANGE && (val == INT64_MAX || val == INT64_MIN)) {
      throw TypeError::New(env, "writeInt64: input String numerical value out of range");
    } else if (errno != 0 && val == 0) {
      char errmsg[200];
      snprintf(errmsg, sizeof(errmsg), "writeInt64: %s", strerror(errno));
      throw TypeError::New(env, errmsg);
    }
  } else {
    throw TypeError::New(env, "writeInt64: Number/String 64-bit value required");
  }

  *reinterpret_cast<int64_t*>(ptr) = val;
}

/**
 * Reads a machine-endian uint64_t from the given Buffer at the given offset.
 *
 * args[0] - Buffer - the "buf" Buffer instance to read from
 * args[1] - Number - the offset from the "buf" buffer's address to read from
 */

Value ReadUInt64(const CallbackInfo& args) {
  Env env = args.Env();
  char* ptr = AddressForArgs(args);

  if (ptr == nullptr) {
    throw TypeError::New(env, "readUInt64: Cannot read from nullptr pointer");
  }

  uint64_t val = *reinterpret_cast<uint64_t*>(ptr);

  if (val > JS_MAX_INT) {
    char strbuf[128];
    snprintf(strbuf, 128, "%" PRIu64, val);
    return String::New(env, strbuf);
  } else {
    return Number::New(env, val);
  }
}

/**
 * Writes the input Number/String uint64 value as a machine-endian uint64_t to
 * the given Buffer at the given offset.
 *
 * args[0] - Buffer - the "buf" Buffer instance to write to
 * args[1] - Number - the offset from the "buf" buffer's address to write to
 * args[2] - String/Number - the "input" String or Number which will be written
 */

void WriteUInt64(const CallbackInfo& args) {
  Env env = args.Env();
  char* ptr = AddressForArgs(args);

  Value in = args[2];
  uint64_t val;
  if (in.IsNumber()) {
    val = static_cast<int64_t>(in.As<Number>());
  } else if (in.IsString()) {
    char* endptr;
    char* str;
    int base = 0;
    std::string _str = in.As<String>();
    str = &_str[0];

    errno = 0;     /* To distinguish success/failure after call */
    val = strtoull(str, &endptr, base);

    if (endptr == str) {
      throw TypeError::New(env, "writeUInt64: no digits we found in input String");
    } else  if (errno == ERANGE && (val == UINT64_MAX)) {
      throw TypeError::New(env, "writeUInt64: input String numerical value out of range");
    } else if (errno != 0 && val == 0) {
      char errmsg[200];
      snprintf(errmsg, sizeof(errmsg), "writeUInt64: %s", strerror(errno));
      throw TypeError::New(env, errmsg);
    }
  } else {
    throw TypeError::New(env, "writeUInt64: Number/String 64-bit value required");
  }

  *reinterpret_cast<uint64_t*>(ptr) = val;
}

/**
 * Reads a Utf8 C String from the given pointer at the given offset (or 0).
 * I didn't want to add this function but it ends up being necessary for reading
 * past a 0 or 1 length Buffer's boundary in node-ffi :\
 *
 * args[0] - Buffer - the "buf" Buffer instance to read from
 * args[1] - Number - the offset from the "buf" buffer's address to read from
 */

Value ReadCString(const CallbackInfo& args) {
  Env env = args.Env();
  char* ptr = AddressForArgs(args);

  if (ptr == nullptr) {
    throw Error::New(env, "readCString: Cannot read from nullptr pointer");
  }

  return String::New(env, ptr);
}

/**
 * Returns a new Buffer instance that has the same memory address
 * as the given buffer, but with the specified size.
 *
 * args[0] - Buffer - the "buf" Buffer instance to read the address from
 * args[1] - Number - the size in bytes that the returned Buffer should be
 * args[2] - Number - the offset from the "buf" buffer's address to read from
 */

Value ReinterpretBuffer(const CallbackInfo& args) {
  Env env = args.Env();
  char* ptr = AddressForArgs(args, 2);

  if (ptr == nullptr) {
    throw Error::New(env, "reinterpret: Cannot reinterpret from nullptr pointer");
  }

  int64_t size = args[1].ToNumber();

  return WrapPointer(env, ptr, size);
}

/**
 * Returns a new Buffer instance that has the same memory address
 * as the given buffer, but with a length up to the first aligned set of values of
 * 0 in a row for the given length.
 *
 * args[0] - Buffer - the "buf" Buffer instance to read the address from
 * args[1] - Number - the number of sequential 0-byte values that need to be read
 * args[2] - Number - the offset from the "buf" buffer's address to read from
 */

Value ReinterpretBufferUntilZeros(const CallbackInfo& args) {
  Env env = args.Env();
  char* ptr = AddressForArgs(args, 2);

  if (ptr == nullptr) {
    throw Error::New(env, "reinterpretUntilZeros: Cannot reinterpret from nullptr pointer");
  }

  uint32_t numZeros = args[1].ToNumber();
  uint32_t i = 0;
  size_t size = 0;
  bool end = false;

  while (!end && size < kMaxLength) {
    end = true;
    for (i = 0; i < numZeros; i++) {
      if (ptr[size + i] != 0) {
        end = false;
        break;
      }
    }
    if (!end) {
      size += numZeros;
    }
  }

  return WrapPointer(env, ptr, size);
}


} // anonymous namespace

Object Init(Env env, Object exports) {
  InstanceData* data = new InstanceData(env);
  {
    Value buffer_ctor = env.Global()["Buffer"];
    Value buffer_from = buffer_ctor.As<Object>()["from"];
    data->buffer_from.Reset(buffer_from.As<Function>(), 1);
    assert(!data->buffer_from.IsEmpty());
    napi_status status = napix_set_instance_data(
        env, data, [](napi_env env, void* data, void* hint) {
          delete static_cast<InstanceData*>(data);
        }, nullptr);
    if (status != napi_ok) {
      delete data;
      data = nullptr;
    } else {
      // Hack around the fact that we can't reset buffer_from from the
      // InstanceData dtor.
      buffer_from.As<Object>().AddFinalizer([](Env env, InstanceData* data) {
        data->buffer_from.Reset();
      }, data);
    }
  }
  exports["instance"] = External<RefNapi::Instance>::New(env, data);

  // "sizeof" map
  Object smap = Object::New(env);
  // fixed sizes
#define SET_SIZEOF(name, type) \
  smap[ #name ] = Number::New(env, sizeof(type));
  SET_SIZEOF(int8, int8_t);
  SET_SIZEOF(uint8, uint8_t);
  SET_SIZEOF(int16, int16_t);
  SET_SIZEOF(uint16, uint16_t);
  SET_SIZEOF(int32, int32_t);
  SET_SIZEOF(uint32, uint32_t);
  SET_SIZEOF(int64, int64_t);
  SET_SIZEOF(uint64, uint64_t);
  SET_SIZEOF(float, float);
  SET_SIZEOF(double, double);
  // (potentially) variable sizes
  SET_SIZEOF(bool, bool);
  SET_SIZEOF(byte, unsigned char);
  SET_SIZEOF(char, char);
  SET_SIZEOF(uchar, unsigned char);
  SET_SIZEOF(short, short);
  SET_SIZEOF(ushort, unsigned short);
  SET_SIZEOF(int, int);
  SET_SIZEOF(uint, unsigned int);
  SET_SIZEOF(long, long);
  SET_SIZEOF(ulong, unsigned long);
  SET_SIZEOF(longlong, long long);
  SET_SIZEOF(ulonglong, unsigned long long);
  SET_SIZEOF(pointer, char *);
  SET_SIZEOF(size_t, size_t);
  // size of a weak handle to a JS object
  SET_SIZEOF(Object, Reference<Object>);

  // "alignof" map
  Object amap = Object::New(env);
#define SET_ALIGNOF(name, type) \
  struct s_##name { type a; }; \
  amap[ #name ] = Number::New(env, alignof(struct s_##name));
  SET_ALIGNOF(int8, int8_t);
  SET_ALIGNOF(uint8, uint8_t);
  SET_ALIGNOF(int16, int16_t);
  SET_ALIGNOF(uint16, uint16_t);
  SET_ALIGNOF(int32, int32_t);
  SET_ALIGNOF(uint32, uint32_t);
  SET_ALIGNOF(int64, int64_t);
  SET_ALIGNOF(uint64, uint64_t);
  SET_ALIGNOF(float, float);
  SET_ALIGNOF(double, double);
  SET_ALIGNOF(bool, bool);
  SET_ALIGNOF(char, char);
  SET_ALIGNOF(uchar, unsigned char);
  SET_ALIGNOF(short, short);
  SET_ALIGNOF(ushort, unsigned short);
  SET_ALIGNOF(int, int);
  SET_ALIGNOF(uint, unsigned int);
  SET_ALIGNOF(long, long);
  SET_ALIGNOF(ulong, unsigned long);
  SET_ALIGNOF(longlong, long long);
  SET_ALIGNOF(ulonglong, unsigned long long);
  SET_ALIGNOF(pointer, char *);
  SET_ALIGNOF(size_t, size_t);
  SET_ALIGNOF(Object, Reference<Object>);

  // exports
  exports["sizeof"] = smap;
  exports["alignof"] = amap;
  exports["nullptr"] = exports["NULL"] = WrapPointer(env, nullptr, 0);
  exports["address"] = Function::New(env, Address);
  exports["hexAddress"] = Function::New(env, HexAddress);
  exports["isNull"] = Function::New(env, IsNull);
  exports["readObject"] = Function::New(env, ReadObject);
  exports["_writeObject"] = Function::New(env, WriteObject);
  exports["readPointer"] = Function::New(env, ReadPointer);
  exports["_writePointer"] = Function::New(env, WritePointer);
  exports["readInt64"] = Function::New(env, ReadInt64);
  exports["writeInt64"] = Function::New(env, WriteInt64);
  exports["readUInt64"] = Function::New(env, ReadUInt64);
  exports["writeUInt64"] = Function::New(env, WriteUInt64);
  exports["readCString"] = Function::New(env, ReadCString);
  exports["_reinterpret"] = Function::New(env, ReinterpretBuffer);
  exports["_reinterpretUntilZeros"] = Function::New(env, ReinterpretBufferUntilZeros);
  return exports;
}

NODE_API_MODULE(binding, Init)
