#include "qjs_enet.h"
#include "qjs_blob.h"
#include <enet/enet.h>
#include <stdio.h>
#include <string.h>
#include <math.h>

#define countof(a) (sizeof(a)/sizeof(*(a)))

static JSClassID enet_host_id;
static JSClassID enet_peer_class_id;

/* Finalizers */
static void js_enet_host_finalizer(JSRuntime *rt, JSValue val)
{
  ENetHost *host = JS_GetOpaque(val, enet_host_id);
  if (host) enet_host_destroy(host);
}

static void js_enet_peer_mark(JSRuntime *rt, JSValueConst val, JS_MarkFunc *mark_func)
{
  ENetPeer *peer = JS_GetOpaque(val, enet_peer_class_id);
  JS_MarkValue(rt, *(JSValue*)peer->data, mark_func);
}

static void js_enet_peer_finalizer(JSRuntime *rt, JSValue val)
{
  ENetPeer *peer = JS_GetOpaque(val, enet_peer_class_id);
  JS_FreeValueRT(rt, *(JSValue*)peer->data);
  free(peer->data);
}

/* ENet init/deinit */
static JSValue js_enet_initialize(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
  if (enet_initialize() != 0) return JS_ThrowInternalError(ctx, "Error initializing ENet");
  return JS_NULL;
}

static JSValue js_enet_deinitialize(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
  enet_deinitialize();
  return JS_NULL;
}

static JSValue js_enet_host_create(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
  ENetHost *host;
  ENetAddress address;
  ENetAddress *send = &address;
  size_t peer_count = 1000;
  size_t channel_limit = 0;
  enet_uint32 incoming_bandwidth = 0;
  enet_uint32 outgoing_bandwidth = 0;
  JSValue obj;

  if (argc < 1 || !JS_IsObject(argv[0])) {
    host = enet_host_create(NULL, peer_count, channel_limit, incoming_bandwidth, outgoing_bandwidth);
    if (!host) return JS_ThrowInternalError(ctx, "Failed to create ENet client host");
    goto wrap;
  }

  JSValue config_obj = argv[0];
  JSValue addr_val = JS_GetPropertyStr(ctx, config_obj, "address");
  const char *addr_str = JS_IsString(addr_val) ? JS_ToCString(ctx, addr_val) : NULL;
  JS_FreeValue(ctx, addr_val);

  if (!addr_str)
    send = NULL;
  else {
    JSValue port_val = JS_GetPropertyStr(ctx, config_obj, "port");
    int32_t port32 = 0;
    JS_ToInt32(ctx, &port32, port_val);
    JS_FreeValue(ctx, port_val);

    if (strcmp(addr_str, "any") == 0)
      address.host = ENET_HOST_ANY;
    else if (strcmp(addr_str, "broadcast") == 0)
      address.host = ENET_HOST_BROADCAST;
    else {
      int err = enet_address_set_host_ip(&address, addr_str);
      if (err != 0) {
        JS_FreeCString(ctx, addr_str);
        return JS_ThrowInternalError(ctx, "Failed to set host IP from '%s'. Error: %d", addr_str, err);
      }
    }
    address.port = (enet_uint16)port32;
    JS_FreeCString(ctx, addr_str);
  }

  JSValue chan_val = JS_GetPropertyStr(ctx, config_obj, "channels");
  JS_ToUint32(ctx, &channel_limit, chan_val);
  JS_FreeValue(ctx, chan_val);

  JSValue in_bw_val = JS_GetPropertyStr(ctx, config_obj, "incoming_bandwidth");
  JS_ToUint32(ctx, &incoming_bandwidth, in_bw_val);
  JS_FreeValue(ctx, in_bw_val);

  JSValue out_bw_val = JS_GetPropertyStr(ctx, config_obj, "outgoing_bandwidth");
  JS_ToUint32(ctx, &outgoing_bandwidth, out_bw_val);
  JS_FreeValue(ctx, out_bw_val);

  host = enet_host_create(send, peer_count, channel_limit, incoming_bandwidth, outgoing_bandwidth);
  if (!host) return JS_ThrowInternalError(ctx, "Failed to create ENet host");

wrap:
  obj = JS_NewObjectClass(ctx, enet_host_id);
  if (JS_IsException(obj)) {
    enet_host_destroy(host);
    return obj;
  }
  JS_SetOpaque(obj, host);
  return obj;
}

static JSValue peer_get_value(JSContext *ctx, ENetPeer *peer)
{
  if (!peer->data) {
    peer->data = malloc(sizeof(JSValue));
    *(JSValue*)peer->data = JS_NewObjectClass(ctx, enet_peer_class_id);
    JS_SetOpaque(*(JSValue*)peer->data, peer);
  }
  return JS_DupValue(ctx, *(JSValue*)peer->data);
}

/* Host service: poll for events */
static JSValue js_enet_host_service(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
  ENetHost *host = JS_GetOpaque(this_val, enet_host_id);
  if (!host) return JS_EXCEPTION;

  if (argc < 1 || !JS_IsFunction(ctx, argv[0])) return JS_ThrowTypeError(ctx, "Expected a callback function as first argument");
  JSValue callback = JS_DupValue(ctx, argv[0]);

  double secs;
  JS_ToFloat64(ctx, &secs, argv[1]);

  ENetEvent event;
  while (enet_host_service(host, &event, secs*1000.0f) > 0) {
    JSValue event_obj = JS_NewObject(ctx);
    JS_SetPropertyStr(ctx, event_obj, "peer", peer_get_value(ctx, event.peer));

    switch (event.type) {
      case ENET_EVENT_TYPE_CONNECT:
        JS_SetPropertyStr(ctx, event_obj, "type", JS_NewString(ctx, "connect"));
        break;
      case ENET_EVENT_TYPE_RECEIVE:
        JS_SetPropertyStr(ctx, event_obj, "type", JS_NewString(ctx, "receive"));
        JS_SetPropertyStr(ctx, event_obj, "channelID", JS_NewInt32(ctx, event.channelID));
        
        // Pass raw data as string or ArrayBuffer
        if (event.packet->dataLength > 0) {
          JSValue data_val = js_new_blob_stoned_copy(ctx, event.packet->data, event.packet->dataLength);
          JS_SetPropertyStr(ctx, event_obj, "data", data_val);
        }
        enet_packet_destroy(event.packet);
        break;
      case ENET_EVENT_TYPE_DISCONNECT:
        JS_SetPropertyStr(ctx, event_obj, "type", JS_NewString(ctx, "disconnect"));
        break;
      case ENET_EVENT_TYPE_NONE:
        JS_SetPropertyStr(ctx, event_obj, "type", JS_NewString(ctx, "none"));
        break;
    }

    uncaught_exception(ctx, JS_Call(ctx, callback, JS_NULL, 1, &event_obj));
    JS_FreeValue(ctx, event_obj);
  }

  JS_FreeValue(ctx, callback);
  return JS_NULL;
}

/* Host connect: client -> connect to server */
static JSValue js_enet_host_connect(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
  ENetHost *host = JS_GetOpaque(this_val, enet_host_id);
  if (!host) return JS_EXCEPTION;

  if (argc < 2) return JS_ThrowTypeError(ctx, "Expected 2 arguments: hostname, port");

  const char *hostname = JS_ToCString(ctx, argv[0]);
  if (!hostname) return JS_EXCEPTION;
  int port;
  JS_ToInt32(ctx, &port, argv[1]);

  ENetAddress address;
  enet_address_set_host(&address, hostname);
  JS_FreeCString(ctx, hostname);
  address.port = port;

  ENetPeer *peer = enet_host_connect(host, &address, 2, 0);
  if (!peer) return JS_ThrowInternalError(ctx, "No available peers for initiating an ENet connection");

  return peer_get_value(ctx, peer);
}

/* Flush queued packets */
static JSValue js_enet_host_flush(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
  ENetHost *host = JS_GetOpaque(this_val, enet_host_id);
  if (!host) return JS_EXCEPTION;
  enet_host_flush(host);
  return JS_NULL;
}

/* Broadcast a string or ArrayBuffer */
static JSValue js_enet_host_broadcast(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
  ENetHost *host = JS_GetOpaque(this_val, enet_host_id);
  if (!host) return JS_EXCEPTION;

  if (argc < 1) return JS_ThrowTypeError(ctx, "Expected a string or ArrayBuffer to broadcast");

  const char *data_str = NULL;
  size_t data_len = 0;
  uint8_t *buf = NULL;

  if (JS_IsString(argv[0])) {
    data_str = JS_ToCStringLen(ctx, &data_len, argv[0]);
    if (!data_str) return JS_EXCEPTION;
  } else if (js_is_blob(ctx,argv[0])) {
    buf = js_get_blob_data(ctx, &data_len, argv[0]);
    if (!buf) return JS_EXCEPTION;
  } else {
    return JS_ThrowTypeError(ctx, "broadcast() only accepts a string or ArrayBuffer");
  }

  ENetPacket *packet = enet_packet_create(data_str ? data_str : buf, data_len, ENET_PACKET_FLAG_RELIABLE);
  if (data_str) JS_FreeCString(ctx, data_str);
  if (!packet) return JS_ThrowInternalError(ctx, "Failed to create ENet packet");

  enet_host_broadcast(host, 0, packet);
  return JS_NULL;
}

static JSValue js_enet_host_get_port(JSContext *js, JSValueConst self, int argc, JSValueConst *argv)
{
  ENetHost *host = JS_GetOpaque(self, enet_host_id);
  if (!host) return JS_EXCEPTION;
  return JS_NewInt32(js, host->address.port);
}

static JSValue js_enet_host_get_address(JSContext *js, JSValueConst self, int argc, JSValueConst *argv)
{
  ENetHost *me = JS_GetOpaque(self, enet_host_id);
  if (!me) return JS_EXCEPTION;

  uint32_t host = ntohl(me->address.host);
  if (host == 0x7F000001) return JS_NewString(js, "localhost");

  char ip_str[16];
  snprintf(ip_str, sizeof(ip_str), "%u.%u.%u.%u",
           (host >> 24) & 0xFF,
           (host >> 16) & 0xFF,
           (host >> 8) & 0xFF,
           host & 0xFF);
  return JS_NewString(js, ip_str);
}

/* Peer-level operations */
static JSValue js_enet_peer_disconnect(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
  ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
  if (!peer) return JS_EXCEPTION;
  enet_peer_disconnect(peer, 0);
  return JS_NULL;
}

/* Peer send must only accept string or ArrayBuffer */
static JSValue js_enet_peer_send(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
  ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
  if (!peer) return JS_EXCEPTION;

  if (argc < 1) return JS_ThrowTypeError(ctx, "Expected a string or ArrayBuffer to send");

  const char *data_str = NULL;
  size_t data_len = 0;
  uint8_t *buf = NULL;

  if (JS_IsString(argv[0])) {
    data_str = JS_ToCStringLen(ctx, &data_len, argv[0]);
    if (!data_str) return JS_EXCEPTION;
  } else if (js_is_blob(ctx,argv[0])) {
    buf = js_get_blob_data(ctx, &data_len, argv[0]);
    if (!buf) return JS_EXCEPTION;
  } else {
    return JS_ThrowTypeError(ctx, "send() only accepts a string or ArrayBuffer");
  }

  ENetPacket *packet = enet_packet_create(data_str ? data_str : buf, data_len, ENET_PACKET_FLAG_RELIABLE);
  if (data_str) JS_FreeCString(ctx, data_str);
  if (!packet) return JS_ThrowInternalError(ctx, "Failed to create ENet packet");

  if (enet_peer_send(peer, 0, packet) < 0) return JS_ThrowInternalError(ctx, "Unable to send packet");
  return JS_NULL;
}

static JSValue js_enet_peer_disconnect_now(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
  ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
  if (!peer) return JS_EXCEPTION;
  enet_peer_disconnect_now(peer, 0);
  return JS_NULL;
}

static JSValue js_enet_peer_disconnect_later(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
  ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
  if (!peer) return JS_EXCEPTION;
  enet_peer_disconnect_later(peer, 0);
  return JS_NULL;
}

static JSValue js_enet_peer_reset(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
  ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
  if (!peer) return JS_EXCEPTION;
  enet_peer_reset(peer);
  return JS_NULL;
}

static JSValue js_enet_peer_ping(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
  ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
  if (!peer) return JS_EXCEPTION;
  enet_peer_ping(peer);
  return JS_NULL;
}

static JSValue js_enet_peer_throttle_configure(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
  ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
  if (!peer) return JS_EXCEPTION;

  int interval, acceleration, deceleration;
  if (argc < 3 || JS_ToInt32(ctx, &interval, argv[0]) || JS_ToInt32(ctx, &acceleration, argv[1]) || JS_ToInt32(ctx, &deceleration, argv[2]))
    return JS_ThrowTypeError(ctx, "Expected 3 int arguments: interval, acceleration, deceleration");

  enet_peer_throttle_configure(peer, interval, acceleration, deceleration);
  return JS_NULL;
}

static JSValue js_enet_peer_timeout(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
  ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
  if (!peer) return JS_EXCEPTION;

  int timeout_limit, timeout_min, timeout_max;
  if (argc < 3 || JS_ToInt32(ctx, &timeout_limit, argv[0]) || JS_ToInt32(ctx, &timeout_min, argv[1]) || JS_ToInt32(ctx, &timeout_max, argv[2]))
    return JS_ThrowTypeError(ctx, "Expected 3 integer arguments: timeout_limit, timeout_min, timeout_max");

  enet_peer_timeout(peer, timeout_limit, timeout_min, timeout_max);
  return JS_NULL;
}

/* Class definitions */
static JSClassDef enet_host = {
  "ENetHost",
  .finalizer = js_enet_host_finalizer,
};

static JSClassDef enet_peer_class = {
  "ENetPeer",
  .finalizer = js_enet_peer_finalizer,
  .gc_mark = js_enet_peer_mark
};

JSValue js_enet_resolve_hostname(JSContext *js, JSValue self, int argc, JSValue *argv)
{
  // TODO: implement
  const char *hostname = JS_ToCString(js, argv[0]);
  JS_FreeCString(js, hostname);
  return JS_NULL;
}

/* Function lists */
static const JSCFunctionListEntry js_enet_funcs[] = {
  JS_CFUNC_DEF("initialize", 0, js_enet_initialize),
  JS_CFUNC_DEF("deinitialize", 0, js_enet_deinitialize),
  JS_CFUNC_DEF("create_host", 1, js_enet_host_create),
  JS_CFUNC_DEF("resolve_hostname", 1, js_enet_resolve_hostname),
};

static const JSCFunctionListEntry js_enet_host_funcs[] = {
  JS_CFUNC_DEF("service", 2, js_enet_host_service),
  JS_CFUNC_DEF("connect", 2, js_enet_host_connect),
  JS_CFUNC_DEF("flush", 0, js_enet_host_flush),
  JS_CFUNC_DEF("broadcast", 1, js_enet_host_broadcast),
  JS_CGETSET_DEF("port", js_enet_host_get_port, NULL),
  JS_CGETSET_DEF("address", js_enet_host_get_address, NULL),
};

/* Peer getters */
static JSValue js_enet_peer_get_rtt(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
  ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
  if (!peer) return JS_EXCEPTION;
  return JS_NewInt32(ctx, peer->roundTripTime);
}

static JSValue js_enet_peer_get_incoming_bandwidth(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
  ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
  if (!peer) return JS_EXCEPTION;
  if (peer->incomingBandwidth == 0) return JS_NewFloat64(ctx, INFINITY);
  return JS_NewInt32(ctx, peer->incomingBandwidth);
}

static JSValue js_enet_peer_get_outgoing_bandwidth(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
  ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
  if (!peer) return JS_EXCEPTION;
  if (peer->outgoingBandwidth == 0) return JS_NewFloat64(ctx, INFINITY);
  return JS_NewInt32(ctx, peer->outgoingBandwidth);
}

static JSValue js_enet_peer_get_last_send_time(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
  ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
  if (!peer) return JS_EXCEPTION;
  return JS_NewInt32(ctx, peer->lastSendTime);
}

static JSValue js_enet_peer_get_last_receive_time(JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv)
{
  ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
  if (!peer) return JS_EXCEPTION;
  return JS_NewInt32(ctx, peer->lastReceiveTime);
}

static JSValue js_enet_peer_get_mtu(JSContext *ctx, JSValueConst this_val)
{
  ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
  if (!peer) return JS_NewFloat64(ctx, INFINITY);
  return JS_NewInt32(ctx, peer->mtu);
}

static JSValue js_enet_peer_get_outgoing_data_total(JSContext *ctx, JSValueConst this_val)
{
  ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
  if (!peer) return JS_NewFloat64(ctx, INFINITY);
  return JS_NewInt32(ctx, peer->outgoingDataTotal);
}

static JSValue js_enet_peer_get_incoming_data_total(JSContext *ctx, JSValueConst this_val)
{
  ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
  if (!peer) return JS_NewFloat64(ctx, INFINITY);
  return JS_NewInt32(ctx, peer->incomingDataTotal);
}

static JSValue js_enet_peer_get_rtt_variance(JSContext *ctx, JSValueConst this_val)
{
  ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
  if (!peer) return JS_NewFloat64(ctx, INFINITY);
  return JS_NewInt32(ctx, peer->roundTripTimeVariance);
}

static JSValue js_enet_peer_get_packet_loss(JSContext *ctx, JSValueConst this_val)
{
  ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
  if (!peer) return JS_NewFloat64(ctx, INFINITY);
  return JS_NewInt32(ctx, peer->packetLoss);
}

static JSValue js_enet_peer_get_state(JSContext *ctx, JSValueConst this_val)
{
  ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
  if (!peer) return JS_NewInt32(ctx, -1);
  return JS_NewInt32(ctx, peer->state);
}

static JSValue js_enet_peer_get_reliable_data_in_transit(JSContext *ctx, JSValueConst this_val)
{
  ENetPeer *peer = JS_GetOpaque(this_val, enet_peer_class_id);
  if (!peer) return JS_NewFloat64(ctx, INFINITY);
  return JS_NewInt32(ctx, peer->reliableDataInTransit);
}

static JSValue js_enet_peer_get_port(JSContext *js, JSValueConst self)
{
  ENetPeer *peer = JS_GetOpaque(self, enet_peer_class_id);
  return JS_NewUint32(js, peer->address.port);
}

static JSValue js_enet_peer_get_address(JSContext *js, JSValueConst self)
{
  ENetPeer *peer = JS_GetOpaque(self, enet_peer_class_id);
  uint32_t host = ntohl(peer->address.host);
  if (host == 0x7F000001) return JS_NewString(js, "localhost");

  char ip_str[16];
  snprintf(ip_str, sizeof(ip_str), "%u.%u.%u.%u",
           (host >> 24) & 0xFF,
           (host >> 16) & 0xFF,
           (host >> 8) & 0xFF,
           host & 0xFF);
  return JS_NewString(js, ip_str);
}

static const JSCFunctionListEntry js_enet_peer_funcs[] = {
  JS_CFUNC_DEF("send", 1, js_enet_peer_send),
  JS_CFUNC_DEF("disconnect", 0, js_enet_peer_disconnect),
  JS_CFUNC_DEF("disconnect_now", 0, js_enet_peer_disconnect_now),
  JS_CFUNC_DEF("disconnect_later", 0, js_enet_peer_disconnect_later),
  JS_CFUNC_DEF("reset", 0, js_enet_peer_reset),
  JS_CFUNC_DEF("ping", 0, js_enet_peer_ping),
  JS_CFUNC_DEF("throttle_configure", 3, js_enet_peer_throttle_configure),
  JS_CFUNC_DEF("timeout", 3, js_enet_peer_timeout),
  JS_CGETSET_DEF("rtt", js_enet_peer_get_rtt, NULL),
  JS_CGETSET_DEF("incoming_bandwidth", js_enet_peer_get_incoming_bandwidth, NULL),
  JS_CGETSET_DEF("outgoing_bandwidth", js_enet_peer_get_outgoing_bandwidth, NULL),
  JS_CGETSET_DEF("last_send_time", js_enet_peer_get_last_send_time, NULL),
  JS_CGETSET_DEF("last_receive_time", js_enet_peer_get_last_receive_time, NULL),
  JS_CGETSET_DEF("mtu", js_enet_peer_get_mtu, NULL),
  JS_CGETSET_DEF("outgoing_data_total", js_enet_peer_get_outgoing_data_total, NULL),
  JS_CGETSET_DEF("incoming_data_total", js_enet_peer_get_incoming_data_total, NULL),
  JS_CGETSET_DEF("rtt_variance", js_enet_peer_get_rtt_variance, NULL),
  JS_CGETSET_DEF("packet_loss", js_enet_peer_get_packet_loss, NULL),
  JS_CGETSET_DEF("state", js_enet_peer_get_state, NULL),
  JS_CGETSET_DEF("reliable_data_in_transit", js_enet_peer_get_reliable_data_in_transit, NULL),
  JS_CGETSET_DEF("port", js_enet_peer_get_port, NULL),
  JS_CGETSET_DEF("address", js_enet_peer_get_address, NULL),
};

JSValue js_enet_use(JSContext *ctx)
{
  JS_NewClassID(&enet_host_id);
  JS_NewClass(JS_GetRuntime(ctx), enet_host_id, &enet_host);
  JSValue host_proto = JS_NewObject(ctx);
  JS_SetPropertyFunctionList(ctx, host_proto, js_enet_host_funcs, countof(js_enet_host_funcs));
  JS_SetClassProto(ctx, enet_host_id, host_proto);

  JS_NewClassID(&enet_peer_class_id);
  JS_NewClass(JS_GetRuntime(ctx), enet_peer_class_id, &enet_peer_class);
  JSValue peer_proto = JS_NewObject(ctx);
  JS_SetPropertyFunctionList(ctx, peer_proto, js_enet_peer_funcs, countof(js_enet_peer_funcs));
  JS_SetClassProto(ctx, enet_peer_class_id, peer_proto);

  JSValue export_obj = JS_NewObject(ctx);
  JS_SetPropertyFunctionList(ctx, export_obj, js_enet_funcs, countof(js_enet_funcs));
  return export_obj;
}
