/*
 * Copyright (c) 2020 Genexis B.V.
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use, copy,
 * modify, merge, publish, distribute, sublicense, and/or sell copies
 * of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
 * BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
 * ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
 * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

#include <stdint.h>
#include <string.h>
#include <quickjs/quickjs.h>
#include <quickjs/quickjs-libc.h>
#include <libwebsockets.h>

#define countof(x) (sizeof(x) / sizeof((x)[0]))

#define CDEF(name) JS_PROP_INT32_DEF(#name, name, JS_PROP_CONFIGURABLE)

#if LWS_LIBRARY_VERSION_NUMBER < 3002000
#define MAX_WAIT 1000
#else
#define MAX_WAIT INT32_MAX
#ifndef LWS_WITH_EXTERNAL_POLL
#error "LWS_WITH_EXTERNAL_POLL is needed for LWS versions >= 3.2.0"
#endif
#if LWS_LIBRARY_VERSION_NUMBER < 4001002
#error "External poll is broken for 3.2.0 <= LWS version < 4.1.2"
#endif
#endif

#define WSI_DATA_USE_OBJECT (1 << 0)
#define WSI_DATA_USE_LINKED (1 << 1)

/* Buffer pool for write operations */
#define BUFFER_POOL_SIZE 8
#define SMALL_BUFFER_SIZE 1024
#define MEDIUM_BUFFER_SIZE 8192
#define LARGE_BUFFER_SIZE 65536

typedef struct {
    uint8_t *buf;
    size_t size;
    int in_use;
} buffer_pool_entry_t;

typedef struct js_lws_wsi_data {
    struct js_lws_wsi_data *next;
    struct lws *wsi;
    JSValue object;
    JSValue context;
    JSValue user;
    uint8_t in_use;
} js_lws_wsi_data_t;

typedef struct {
    struct lws_context *context;
    JSContext *ctx;
    JSValue callback;
    js_lws_wsi_data_t *wsi_list;
    buffer_pool_entry_t buffer_pool[BUFFER_POOL_SIZE];
} js_lws_context_data_t;

static JSClassID js_lws_context_class_id;
static JSClassID js_lws_wsi_class_id;

/* Buffer pool management */
static void init_buffer_pool(js_lws_context_data_t *data)
{
    int i;
    size_t sizes[] = {SMALL_BUFFER_SIZE, SMALL_BUFFER_SIZE, MEDIUM_BUFFER_SIZE,
                      MEDIUM_BUFFER_SIZE, MEDIUM_BUFFER_SIZE, MEDIUM_BUFFER_SIZE,
                      LARGE_BUFFER_SIZE, LARGE_BUFFER_SIZE};

    for (i = 0; i < BUFFER_POOL_SIZE; i++) {
        data->buffer_pool[i].size = sizes[i];
        data->buffer_pool[i].buf = malloc(LWS_PRE + sizes[i]);
        data->buffer_pool[i].in_use = 0;
    }
}

static void cleanup_buffer_pool(js_lws_context_data_t *data)
{
    int i;
    for (i = 0; i < BUFFER_POOL_SIZE; i++) {
        if (data->buffer_pool[i].buf) {
            free(data->buffer_pool[i].buf);
            data->buffer_pool[i].buf = NULL;
        }
    }
}

static uint8_t* acquire_buffer(js_lws_context_data_t *data, size_t size, size_t *out_size)
{
    int i;
    /* Try to find suitable buffer from pool */
    for (i = 0; i < BUFFER_POOL_SIZE; i++) {
        if (!data->buffer_pool[i].in_use && data->buffer_pool[i].size >= size) {
            data->buffer_pool[i].in_use = 1;
            *out_size = data->buffer_pool[i].size;
            return data->buffer_pool[i].buf;
        }
    }
    /* No suitable buffer found, allocate new one */
    *out_size = size;
    return malloc(LWS_PRE + size);
}

static void release_buffer(js_lws_context_data_t *data, uint8_t *buf)
{
    int i;
    /* Check if buffer is from pool */
    for (i = 0; i < BUFFER_POOL_SIZE; i++) {
        if (data->buffer_pool[i].buf == buf) {
            data->buffer_pool[i].in_use = 0;
            return;
        }
    }
    /* Not from pool, free it */
    free(buf);
}

static void free_wsi_data_rt(JSRuntime *rt, js_lws_wsi_data_t *data)
{
    JS_FreeValueRT(rt, data->object);
    JS_FreeValueRT(rt, data->context);
    JS_FreeValueRT(rt, data->user);
    js_free_rt(rt, data);
}

static void unlink_wsi_rt(JSRuntime *rt, js_lws_context_data_t *data,
                          js_lws_wsi_data_t *wsi_data)
{
    js_lws_wsi_data_t **p;
    for (p = &data->wsi_list; *p; p = &(*p)->next) {
        if (*p == wsi_data) {
            *p = (*p)->next;
            break;
        }
    }
    wsi_data->next = NULL;
    wsi_data->wsi = NULL;
    JS_FreeValueRT(rt, wsi_data->object);
    wsi_data->object = JS_UNDEFINED;
    wsi_data->in_use &= ~WSI_DATA_USE_LINKED;
    if (wsi_data->in_use == 0)
        free_wsi_data_rt(rt, wsi_data);
}

static void unlink_wsi(JSContext *ctx, js_lws_context_data_t *data,
                       js_lws_wsi_data_t *wsi_data)
{
    unlink_wsi_rt(JS_GetRuntime(ctx), data, wsi_data);
}

static JSValue convert_pollargs(JSContext *ctx, const struct lws_pollargs *pa)
{
    JSValue obj;

    if (pa == NULL)
        return JS_NULL;

    obj = JS_NewObject(ctx);
    if (JS_IsException(obj))
        return obj;

    if (JS_SetPropertyStr(ctx, obj, "fd", JS_NewInt32(ctx, pa->fd)) < 0
        || JS_SetPropertyStr(ctx, obj, "events",
                             JS_NewInt32(ctx, pa->events)) < 0
        || JS_SetPropertyStr(ctx, obj, "prev_events",
                             JS_NewInt32(ctx, pa->prev_events)) < 0) {
        JS_FreeValue(ctx, obj);
        return JS_EXCEPTION;
    }

    return obj;
}

static JSValue convert_close_payload(JSContext *ctx,
                                     uint8_t *payload, size_t len)
{
    JSValue array;
    uint16_t status;
    JSValue reason;

    if (payload == NULL || len < 2)
        return JS_NULL;

    array = JS_NewArray(ctx);
    if (JS_IsException(array))
        return array;

    status = (payload[0] << 8) | payload[1];
    if (JS_SetPropertyUint32(ctx, array, 0, JS_NewInt32(ctx, status)) < 0) {
        JS_FreeValue(ctx, array);
        return JS_EXCEPTION;
    }

    reason = JS_NewStringLen(ctx, (const char *)(payload + 2), len - 2);
    if (JS_IsException(reason)
        || JS_SetPropertyUint32(ctx, array, 1, reason) < 0) {
        JS_FreeValue(ctx, array);
        return JS_EXCEPTION;
    }

    return array;
}

static int client_callback(struct lws *wsi, enum lws_callback_reasons reason,
                           void *user, void *in, size_t len)
{
    js_lws_context_data_t *data = lws_context_user(lws_get_context(wsi));
    js_lws_wsi_data_t *wsi_data = lws_wsi_user(wsi);
    JSContext *ctx;
    JSValue args[3];
    JSValue ret;
    int32_t ret_int;
    int i;

    if (data == NULL || data->ctx == NULL || JS_IsUndefined(data->callback))
        return 0;

    ctx = data->ctx;
    args[0] = wsi_data ? JS_DupValue(ctx, wsi_data->object) : JS_NULL;
    args[1] = JS_NewInt32(ctx, reason);
    args[2] = JS_NULL;

    switch (reason) {
    case LWS_CALLBACK_ADD_POLL_FD:
    case LWS_CALLBACK_DEL_POLL_FD:
    case LWS_CALLBACK_CHANGE_MODE_POLL_FD:
        args[2] = convert_pollargs(ctx, in);
        break;
    case LWS_CALLBACK_CLIENT_CONNECTION_ERROR:
        if (in)
            args[2] = JS_NewStringLen(ctx, in, len);
        break;
    case LWS_CALLBACK_WS_PEER_INITIATED_CLOSE:
        args[2] = convert_close_payload(ctx, in, len);
        break;
    case LWS_CALLBACK_RECEIVE:
    case LWS_CALLBACK_CLIENT_RECEIVE:
        if (in)
            args[2] = JS_NewArrayBufferCopy(ctx, in, len);
        break;
    case LWS_CALLBACK_WSI_DESTROY:
        if (wsi_data)
            unlink_wsi(ctx, data, wsi_data);
        break;
    default:
        break;
    }

    if (JS_IsException(args[2]))
        ret = JS_EXCEPTION;
    else
        ret = JS_Call(ctx, data->callback, JS_UNDEFINED, countof(args), args);

    if (JS_IsException(ret) || JS_ToInt32(ctx, &ret_int, ret) < 0) {
        js_std_dump_error(ctx);
        ret_int = -1;
    }

    JS_FreeValue(ctx, ret);
    for (i = 0; i < countof(args); i++) {
        JS_FreeValue(ctx, args[i]);
    }

    return ret_int;
}

static const struct lws_protocols client_protocols[] = {
    { "lws-client", client_callback, 0, 0, 0, NULL, 0 },
    { NULL, NULL, 0, 0, 0, NULL, 0 }
};

static JSValue js_decode_utf8(JSContext *ctx, JSValueConst this_val,
                              int argc, JSValueConst *argv)
{
    size_t size;
    uint8_t *ptr = JS_GetArrayBuffer(ctx, &size, argv[0]);
    if (ptr == NULL)
        return JS_EXCEPTION;
    return JS_NewStringLen(ctx, (const char *)ptr, size);
}

static JSValue js_lws_set_log_level(JSContext *ctx, JSValueConst this_val,
                                    int argc, JSValueConst *argv)
{
    int32_t level;
    if (JS_ToInt32(ctx, &level, argv[0]) < 0)
        return JS_EXCEPTION;
    lws_set_log_level(level, NULL);
    return JS_UNDEFINED;
}

static JSValue js_lws_create_context(JSContext *ctx, JSValueConst this_val,
                                     int argc, JSValueConst *argv)
{
    int secure;
    JSValue obj;
    js_lws_context_data_t *data;
    struct lws_context_creation_info info;
    struct lws_context *context;

    if (!JS_IsFunction(ctx, argv[0]))
        return JS_ThrowTypeError(ctx, "not a function");

    secure = JS_ToBool(ctx, argv[1]);
    if (secure < 0)
        return JS_EXCEPTION;

    obj = JS_NewObjectClass(ctx, js_lws_context_class_id);
    if (JS_IsException(obj))
        return obj;

    data = js_mallocz(ctx, sizeof(js_lws_context_data_t));
    if (data == NULL) {
        JS_FreeValue(ctx, obj);
        return JS_EXCEPTION;
    }
    data->callback = JS_UNDEFINED;

    memset(&info, 0, sizeof(info));
    info.port = CONTEXT_PORT_NO_LISTEN;
    info.protocols = client_protocols;
    info.gid = -1;
    info.uid = -1;
    info.options = secure ? LWS_SERVER_OPTION_DO_SSL_GLOBAL_INIT : 0;
    info.user = data;
    context = lws_create_context(&info);
    if (context == NULL) {
        JS_FreeValue(ctx, obj);
        js_free(ctx, data);
        return JS_ThrowOutOfMemory(ctx);
    }

    data->context = context;
    data->ctx = JS_DupContext(ctx);
    data->callback = JS_DupValue(ctx, argv[0]);
    init_buffer_pool(data);
    JS_SetOpaque(obj, data);

    return obj;
}

static void js_lws_context_finalizer(JSRuntime *rt, JSValue val)
{
    js_lws_context_data_t *data = JS_GetOpaque(val, js_lws_context_class_id);
    if (data) {
        JS_FreeContext(data->ctx);
        data->ctx = NULL;
        JS_FreeValueRT(rt, data->callback);
        data->callback = JS_UNDEFINED;

        lws_context_destroy(data->context);

        while (data->wsi_list) {
            unlink_wsi_rt(rt, data, data->wsi_list);
        }

        cleanup_buffer_pool(data);
        js_free_rt(rt, data);
    }
}

static void js_lws_context_mark(JSRuntime *rt, JSValue val,
                                JS_MarkFunc *mark_func)
{
    js_lws_context_data_t *data = JS_GetOpaque(val, js_lws_context_class_id);
    if (data) {
        js_lws_wsi_data_t *wd;
        mark_func(rt, (JSGCObjectHeader *)data->ctx);
        JS_MarkValue(rt, data->callback, mark_func);
        for (wd = data->wsi_list; wd; wd = wd->next) {
            JS_MarkValue(rt, wd->object, mark_func);
        }
    }
}

static JSValue js_lws_context_get_connections(JSContext *ctx,
                                              JSValueConst this_val)
{
    js_lws_context_data_t *data = JS_GetOpaque2(ctx, this_val,
                                                js_lws_context_class_id);
    int32_t connections = 0;
    js_lws_wsi_data_t *wd;

    if (data == NULL)
        return JS_EXCEPTION;

    for (wd = data->wsi_list; wd; wd = wd->next) {
        connections++;
    }

    return JS_NewInt32(ctx, connections);
}

static JSValue js_lws_client_connect(JSContext *ctx, JSValueConst this_val,
                                     int argc, JSValueConst *argv)
{
    js_lws_context_data_t *data = JS_GetOpaque2(ctx, this_val,
                                                js_lws_context_class_id);
    const char *address = NULL;
    int32_t port;
    int secure;
    const char *path = NULL, *host = NULL, *origin = NULL, *protocol = NULL;
    JSValue obj = JS_UNDEFINED, ret = JS_EXCEPTION;
    js_lws_wsi_data_t *wsi_data;
    struct lws_client_connect_info info;

    if (data == NULL)
        return JS_EXCEPTION;

    address = JS_ToCString(ctx, argv[0]);
    if (address == NULL)
        goto exception;

    if (JS_ToInt32(ctx, &port, argv[1]) < 0)
        goto exception;

    if (port < 1 || port > 65535) {
        JS_ThrowRangeError(ctx, "port must be between 1 and 65535");
        goto exception;
    }

    secure = JS_ToBool(ctx, argv[2]);
    if (secure < 0)
        goto exception;

    path = JS_ToCString(ctx, argv[3]);
    if (path == NULL)
        goto exception;

    host = JS_ToCString(ctx, argv[4]);
    if (host == NULL)
        goto exception;

    if (!JS_IsUndefined(argv[5]) && !JS_IsNull(argv[5])) {
        origin = JS_ToCString(ctx, argv[5]);
        if (origin == NULL)
            goto exception;
    }

    if (!JS_IsUndefined(argv[6]) && !JS_IsNull(argv[6])) {
        protocol = JS_ToCString(ctx, argv[6]);
        if (protocol == NULL)
            goto exception;
    }

    obj = JS_NewObjectClass(ctx, js_lws_wsi_class_id);
    if (JS_IsException(obj))
        goto exception;

    wsi_data = js_mallocz(ctx, sizeof(js_lws_wsi_data_t));
    if (wsi_data == NULL)
        goto exception;
    wsi_data->next = data->wsi_list;
    wsi_data->object = JS_DupValue(ctx, obj);
    wsi_data->context = JS_DupValue(ctx, this_val);
    wsi_data->user = JS_DupValue(ctx, argv[7]);
    wsi_data->in_use = WSI_DATA_USE_OBJECT |  WSI_DATA_USE_LINKED;
    data->wsi_list = wsi_data;
    JS_SetOpaque(obj, wsi_data);

    memset(&info, 0, sizeof(info));
    info.context = data->context;
    info.address = address;
    info.port = port;
    info.ssl_connection = secure ? LCCSCF_USE_SSL : 0;
    info.local_protocol_name = "lws-client";
    info.path = path;
    info.host = host;
    info.origin = origin;
    info.protocol = protocol;
    info.ietf_version_or_minus_one = -1;
    info.userdata = wsi_data;
    info.pwsi = &wsi_data->wsi;
    lws_client_connect_via_info(&info);

    if (wsi_data->wsi) {
        ret = JS_DupValue(ctx, obj);
    } else {
        unlink_wsi(ctx, data, wsi_data);
        JS_ThrowReferenceError(ctx, "cannot connect to [%s]:%d (%s)",
                               address, port, secure ? "wss" : "ws");
    }

exception:
    JS_FreeCString(ctx, address);
    JS_FreeCString(ctx, path);
    JS_FreeCString(ctx, host);
    JS_FreeCString(ctx, origin);
    JS_FreeCString(ctx, protocol);
    JS_FreeValue(ctx, obj);

    return ret;
}

static JSValue js_lws_service_fd(JSContext *ctx, JSValueConst this_val,
                                 int argc, JSValueConst *argv)
{
    js_lws_context_data_t *data = JS_GetOpaque2(ctx, this_val,
                                                js_lws_context_class_id);
    int32_t fd, events, revents;
    struct lws_pollfd pfd;

    if (data == NULL)
        return JS_EXCEPTION;

    if (JS_ToInt32(ctx, &fd, argv[0]) < 0)
        return JS_EXCEPTION;

    if (JS_ToInt32(ctx, &events, argv[1]) < 0)
        return JS_EXCEPTION;

    if (JS_ToInt32(ctx, &revents, argv[2]) < 0)
        return JS_EXCEPTION;

    pfd.fd = fd;
    pfd.events = events;
    pfd.revents = revents;
    lws_service_fd(data->context, &pfd);

    return JS_NewInt32(ctx,
                       lws_service_adjust_timeout(data->context, MAX_WAIT, 0));
}

static JSValue js_lws_service_periodic(JSContext *ctx, JSValueConst this_val,
                                       int argc, JSValueConst *argv)
{
    js_lws_context_data_t *data = JS_GetOpaque2(ctx, this_val,
                                                js_lws_context_class_id);
    int timeout;

    if (data == NULL)
        return JS_EXCEPTION;

#if LWS_LIBRARY_VERSION_NUMBER < 3002000
    lws_service_fd(data->context, NULL);
#endif

    timeout = lws_service_adjust_timeout(data->context, MAX_WAIT, 0);
    if (timeout == 0) {
        lws_service(data->context, -1);
        timeout = lws_service_adjust_timeout(data->context, MAX_WAIT, 0);
    }

    return JS_NewInt32(ctx, timeout);
}

static void js_lws_wsi_finalizer(JSRuntime *rt, JSValue val)
{
    js_lws_wsi_data_t *data = JS_GetOpaque(val, js_lws_wsi_class_id);
    if (data) {
        JS_FreeValueRT(rt, data->context);
        data->context = JS_UNDEFINED;
        JS_FreeValueRT(rt, data->user);
        data->user = JS_UNDEFINED;

        data->in_use &= ~WSI_DATA_USE_OBJECT;
        if (data->in_use == 0)
            free_wsi_data_rt(rt, data);
    }
}

static void js_lws_wsi_mark(JSRuntime *rt, JSValue val,
                            JS_MarkFunc *mark_func)
{
    js_lws_wsi_data_t *data = JS_GetOpaque(val, js_lws_wsi_class_id);
    if (data) {
        JS_MarkValue(rt, data->context, mark_func);
        JS_MarkValue(rt, data->user, mark_func);
    }
}

static JSValue js_lws_wsi_get_context(JSContext *ctx, JSValueConst this_val)
{
    js_lws_wsi_data_t *data = JS_GetOpaque2(ctx, this_val, js_lws_wsi_class_id);
    if (data == NULL)
        return JS_EXCEPTION;
    return JS_DupValue(ctx, data->context);
}

static JSValue js_lws_wsi_get_user(JSContext *ctx, JSValueConst this_val)
{
    js_lws_wsi_data_t *data = JS_GetOpaque2(ctx, this_val, js_lws_wsi_class_id);
    if (data == NULL)
        return JS_EXCEPTION;
    return JS_DupValue(ctx, data->user);
}

static JSValue js_lws_wsi_get_hdr(JSContext *ctx, JSValueConst this_val,
                                  int magic)
{
    js_lws_wsi_data_t *data = JS_GetOpaque2(ctx, this_val, js_lws_wsi_class_id);
    int len;
    char *str;
    JSValue ret;

    if (data == NULL)
        return JS_EXCEPTION;

    if (data->wsi == NULL)
        return JS_ThrowTypeError(ctx, "defunct WSI");

    len = lws_hdr_total_length(data->wsi, magic);
    if (len < 0)
        return JS_ThrowReferenceError(ctx, "HTTP headers unavailable");

    len++;
    str = js_mallocz(ctx, len);
    if (str == NULL)
        return JS_EXCEPTION;

    len = lws_hdr_copy(data->wsi, str, len, magic);
    if (len < 0)
        ret = JS_ThrowReferenceError(ctx, "HTTP headers unavailable");
    else
        ret = JS_NewStringLen(ctx, str, len);

    js_free(ctx, str);

    return ret;
}

static JSValue js_lws_is_final_fragment(JSContext *ctx, JSValueConst this_val,
                                        int argc, JSValueConst *argv)
{
    js_lws_wsi_data_t *data = JS_GetOpaque2(ctx, this_val, js_lws_wsi_class_id);
    if (data == NULL)
        return JS_EXCEPTION;
    if (data->wsi == NULL)
        return JS_ThrowTypeError(ctx, "defunct WSI");
    return JS_NewBool(ctx, lws_is_final_fragment(data->wsi));
}

static JSValue js_lws_is_first_fragment(JSContext *ctx, JSValueConst this_val,
                                        int argc, JSValueConst *argv)
{
    js_lws_wsi_data_t *data = JS_GetOpaque2(ctx, this_val, js_lws_wsi_class_id);
    if (data == NULL)
        return JS_EXCEPTION;
    if (data->wsi == NULL)
        return JS_ThrowTypeError(ctx, "defunct WSI");
    return JS_NewBool(ctx, lws_is_first_fragment(data->wsi));
}

static JSValue js_lws_frame_is_binary(JSContext *ctx, JSValueConst this_val,
                                      int argc, JSValueConst *argv)
{
    js_lws_wsi_data_t *data = JS_GetOpaque2(ctx, this_val, js_lws_wsi_class_id);
    if (data == NULL)
        return JS_EXCEPTION;
    if (data->wsi == NULL)
        return JS_ThrowTypeError(ctx, "defunct WSI");
    return JS_NewBool(ctx, lws_frame_is_binary(data->wsi));
}

static JSValue js_lws_callback_on_writable(JSContext *ctx,
                                           JSValueConst this_val,
                                           int argc, JSValueConst *argv)
{
    js_lws_wsi_data_t *data = JS_GetOpaque2(ctx, this_val, js_lws_wsi_class_id);
    if (data == NULL)
        return JS_EXCEPTION;
    if (data->wsi == NULL)
        return JS_ThrowTypeError(ctx, "defunct WSI");
    lws_callback_on_writable(data->wsi);
    return JS_UNDEFINED;
}

static JSValue js_lws_write(JSContext *ctx, JSValueConst this_val,
                            int argc, JSValueConst *argv)
{
    js_lws_wsi_data_t *wsi_data = JS_GetOpaque2(ctx, this_val, js_lws_wsi_class_id);
    js_lws_context_data_t *ctx_data;
    const uint8_t *ptr;
    uint8_t *buf;
    uint8_t stack_buf[1024 + LWS_PRE];
    size_t size, buf_size;
    enum lws_write_protocol protocol;
    int ret;
    int use_pool = 0;

    if (wsi_data == NULL)
        return JS_EXCEPTION;

    if (wsi_data->wsi == NULL)
        return JS_ThrowTypeError(ctx, "defunct WSI");

    ctx_data = lws_context_user(lws_get_context(wsi_data->wsi));

    if (JS_IsString(argv[0])) {
        /* Try zero-copy path: get direct pointer to QuickJS string buffer
         * This avoids allocation and UTF-8 conversion if string is already UTF-8 */
        ptr = (const uint8_t *)JS_ToCStringLen(ctx, &size, argv[0]);
        if (ptr == NULL)
            return JS_EXCEPTION;
        protocol = LWS_WRITE_TEXT;

        /* For strings, we can write directly from the QuickJS buffer if small enough
         * to avoid extra memcpy */
        if (size <= 1024) {
            /* Small strings: copy to stack buffer (one copy) */
            buf = stack_buf;
            memcpy(buf + LWS_PRE, ptr, size);
            JS_FreeCString(ctx, (const char *)ptr);
        } else {
            /* Large strings: use pool buffer (one copy) */
            buf = acquire_buffer(ctx_data, size, &buf_size);
            use_pool = 1;
            if (buf == NULL) {
                JS_FreeCString(ctx, (const char *)ptr);
                return JS_EXCEPTION;
            }
            memcpy(buf + LWS_PRE, ptr, size);
            JS_FreeCString(ctx, (const char *)ptr);
        }
    } else {
        /* Binary data path */
        ptr = JS_GetArrayBuffer(ctx, &size, argv[0]);
        if (ptr == NULL)
            return JS_EXCEPTION;
        protocol = LWS_WRITE_BINARY;

        /* Use stack buffer for small messages */
        if (size <= 1024) {
            buf = stack_buf;
        } else {
            /* Try to get buffer from pool */
            buf = acquire_buffer(ctx_data, size, &buf_size);
            use_pool = 1;
            if (buf == NULL)
                return JS_EXCEPTION;
        }
        memcpy(buf + LWS_PRE, ptr, size);
    }

    ret = lws_write(wsi_data->wsi, buf + LWS_PRE, size, protocol);

    /* Release buffer back to pool or free if not from pool */
    if (use_pool)
        release_buffer(ctx_data, buf);

    if (ret < 0)
        return JS_ThrowTypeError(ctx, "WSI not writable");

    return JS_UNDEFINED;
}

static JSValue js_lws_close_reason(JSContext *ctx, JSValueConst this_val,
                                   int argc, JSValueConst *argv)
{
    js_lws_wsi_data_t *data = JS_GetOpaque2(ctx, this_val, js_lws_wsi_class_id);
    int32_t status;
    const char *reason = NULL;
    size_t len = 0;

    if (data == NULL)
        return JS_EXCEPTION;

    if (data->wsi == NULL)
        return JS_ThrowTypeError(ctx, "defunct WSI");

    if (JS_ToInt32(ctx, &status, argv[0]) < 0)
        return JS_EXCEPTION;

    if (status < 0 || status > 65535)
        return JS_ThrowRangeError(ctx, "status must be between 0 and 65535");

    if (!JS_IsUndefined(argv[1])) {
        reason = JS_ToCStringLen(ctx, &len, argv[1]);
        if (reason == NULL)
            return JS_EXCEPTION;
        if (len > 123) {
            JS_FreeCString(ctx, reason);
            return JS_ThrowTypeError(ctx, "reason too long (%zu > 123)", len);
        }
    }

    lws_close_reason(data->wsi, status, (uint8_t *)reason, len);

    if (reason)
        JS_FreeCString(ctx, reason);

    return JS_UNDEFINED;
}

/* Parse WebSocket URL in C for better performance
 * Returns object: { secure: bool, address: string, port: number, path: string }
 * Throws TypeError on invalid URL */
static JSValue js_lws_parse_url(JSContext *ctx, JSValueConst this_val,
                                int argc, JSValueConst *argv)
{
    const char *url;
    size_t url_len;
    char *host_start, *host_end, *path_start;
    char address[256];
    char path[1024];
    int secure = 0;
    int port = 0;
    JSValue result;

    url = JS_ToCStringLen(ctx, &url_len, argv[0]);
    if (url == NULL)
        return JS_EXCEPTION;

    /* Parse scheme: ws:// or wss:// */
    if (url_len < 5 || (strncasecmp(url, "ws://", 5) != 0 && strncasecmp(url, "wss://", 6) != 0)) {
        JS_FreeCString(ctx, url);
        return JS_ThrowTypeError(ctx, "invalid WebSocket URL");
    }

    if (strncasecmp(url, "wss://", 6) == 0) {
        secure = 1;
        host_start = (char *)url + 6;
    } else {
        host_start = (char *)url + 5;
    }

    /* Find end of host (start of path or end of string) */
    path_start = strchr(host_start, '/');
    if (path_start == NULL) {
        path_start = strchr(host_start, '?');
    }
    if (path_start == NULL) {
        path_start = (char *)url + url_len;
    }

    host_end = path_start;

    /* Extract path (everything after host) */
    if (*path_start == '\0') {
        strcpy(path, "/");
    } else if (*path_start != '/') {
        path[0] = '/';
        strncpy(path + 1, path_start, sizeof(path) - 2);
        path[sizeof(path) - 1] = '\0';
    } else {
        strncpy(path, path_start, sizeof(path) - 1);
        path[sizeof(path) - 1] = '\0';
    }

    /* Parse host and port */
    if (*host_start == '[') {
        /* IPv6 address */
        char *bracket_end = strchr(host_start, ']');
        if (bracket_end == NULL || bracket_end > host_end) {
            JS_FreeCString(ctx, url);
            return JS_ThrowTypeError(ctx, "invalid WebSocket URL");
        }

        size_t addr_len = bracket_end - host_start - 1;
        if (addr_len >= sizeof(address)) {
            JS_FreeCString(ctx, url);
            return JS_ThrowTypeError(ctx, "invalid WebSocket URL");
        }

        strncpy(address, host_start + 1, addr_len);
        address[addr_len] = '\0';

        /* Check for port after bracket */
        if (*(bracket_end + 1) == ':') {
            port = atoi(bracket_end + 2);
        } else {
            port = secure ? 443 : 80;
        }
    } else {
        /* IPv4 or hostname */
        char *colon = strchr(host_start, ':');
        size_t addr_len;

        if (colon != NULL && colon < host_end) {
            addr_len = colon - host_start;
            port = atoi(colon + 1);
        } else {
            addr_len = host_end - host_start;
            port = secure ? 443 : 80;
        }

        if (addr_len >= sizeof(address)) {
            JS_FreeCString(ctx, url);
            return JS_ThrowTypeError(ctx, "invalid WebSocket URL");
        }

        strncpy(address, host_start, addr_len);
        address[addr_len] = '\0';
    }

    /* Validate port range */
    if (port < 1 || port > 65535) {
        JS_FreeCString(ctx, url);
        return JS_ThrowRangeError(ctx, "port must be between 1 and 65535");
    }

    JS_FreeCString(ctx, url);

    /* Return parsed result as object */
    result = JS_NewObject(ctx);
    JS_SetPropertyStr(ctx, result, "secure", JS_NewBool(ctx, secure));
    JS_SetPropertyStr(ctx, result, "address", JS_NewString(ctx, address));
    JS_SetPropertyStr(ctx, result, "port", JS_NewInt32(ctx, port));
    JS_SetPropertyStr(ctx, result, "path", JS_NewString(ctx, path));

    return result;
}

static const JSCFunctionListEntry js_lws_funcs[] = {
    CDEF(LLL_ERR),
    CDEF(LLL_WARN),
    CDEF(LLL_NOTICE),
    CDEF(LLL_INFO),
    CDEF(LLL_DEBUG),
    CDEF(LLL_PARSER),
    CDEF(LLL_HEADER),
    CDEF(LLL_EXT),
    CDEF(LLL_CLIENT),
    CDEF(LLL_LATENCY),
    CDEF(LLL_USER),
    CDEF(LLL_THREAD),
    CDEF(LLL_COUNT),
    CDEF(LWS_CALLBACK_PROTOCOL_INIT),
    CDEF(LWS_CALLBACK_PROTOCOL_DESTROY),
    CDEF(LWS_CALLBACK_WSI_CREATE),
    CDEF(LWS_CALLBACK_WSI_DESTROY),
    CDEF(LWS_CALLBACK_OPENSSL_LOAD_EXTRA_CLIENT_VERIFY_CERTS),
    CDEF(LWS_CALLBACK_OPENSSL_LOAD_EXTRA_SERVER_VERIFY_CERTS),
    CDEF(LWS_CALLBACK_OPENSSL_PERFORM_CLIENT_CERT_VERIFICATION),
    CDEF(LWS_CALLBACK_OPENSSL_CONTEXT_REQUIRES_PRIVATE_KEY),
    CDEF(LWS_CALLBACK_SSL_INFO),
    CDEF(LWS_CALLBACK_OPENSSL_PERFORM_SERVER_CERT_VERIFICATION),
    CDEF(LWS_CALLBACK_SERVER_NEW_CLIENT_INSTANTIATED),
    CDEF(LWS_CALLBACK_HTTP),
    CDEF(LWS_CALLBACK_HTTP_BODY),
    CDEF(LWS_CALLBACK_HTTP_BODY_COMPLETION),
    CDEF(LWS_CALLBACK_HTTP_FILE_COMPLETION),
    CDEF(LWS_CALLBACK_HTTP_WRITEABLE),
    CDEF(LWS_CALLBACK_CLOSED_HTTP),
    CDEF(LWS_CALLBACK_FILTER_HTTP_CONNECTION),
    CDEF(LWS_CALLBACK_ADD_HEADERS),
    CDEF(LWS_CALLBACK_CHECK_ACCESS_RIGHTS),
    CDEF(LWS_CALLBACK_PROCESS_HTML),
    CDEF(LWS_CALLBACK_HTTP_BIND_PROTOCOL),
    CDEF(LWS_CALLBACK_HTTP_DROP_PROTOCOL),
    CDEF(LWS_CALLBACK_HTTP_CONFIRM_UPGRADE),
    CDEF(LWS_CALLBACK_ESTABLISHED_CLIENT_HTTP),
    CDEF(LWS_CALLBACK_CLOSED_CLIENT_HTTP),
    CDEF(LWS_CALLBACK_RECEIVE_CLIENT_HTTP_READ),
    CDEF(LWS_CALLBACK_RECEIVE_CLIENT_HTTP),
    CDEF(LWS_CALLBACK_COMPLETED_CLIENT_HTTP),
    CDEF(LWS_CALLBACK_CLIENT_HTTP_WRITEABLE),
    CDEF(LWS_CALLBACK_CLIENT_HTTP_BIND_PROTOCOL),
    CDEF(LWS_CALLBACK_CLIENT_HTTP_DROP_PROTOCOL),
    CDEF(LWS_CALLBACK_ESTABLISHED),
    CDEF(LWS_CALLBACK_CLOSED),
    CDEF(LWS_CALLBACK_SERVER_WRITEABLE),
    CDEF(LWS_CALLBACK_RECEIVE),
    CDEF(LWS_CALLBACK_RECEIVE_PONG),
    CDEF(LWS_CALLBACK_WS_PEER_INITIATED_CLOSE),
    CDEF(LWS_CALLBACK_FILTER_PROTOCOL_CONNECTION),
    CDEF(LWS_CALLBACK_CONFIRM_EXTENSION_OKAY),
    CDEF(LWS_CALLBACK_WS_SERVER_BIND_PROTOCOL),
    CDEF(LWS_CALLBACK_WS_SERVER_DROP_PROTOCOL),
    CDEF(LWS_CALLBACK_CLIENT_CONNECTION_ERROR),
    CDEF(LWS_CALLBACK_CLIENT_FILTER_PRE_ESTABLISH),
    CDEF(LWS_CALLBACK_CLIENT_ESTABLISHED),
    CDEF(LWS_CALLBACK_CLIENT_CLOSED),
    CDEF(LWS_CALLBACK_CLIENT_APPEND_HANDSHAKE_HEADER),
    CDEF(LWS_CALLBACK_CLIENT_RECEIVE),
    CDEF(LWS_CALLBACK_CLIENT_RECEIVE_PONG),
    CDEF(LWS_CALLBACK_CLIENT_WRITEABLE),
    CDEF(LWS_CALLBACK_CLIENT_CONFIRM_EXTENSION_SUPPORTED),
    CDEF(LWS_CALLBACK_WS_EXT_DEFAULTS),
    CDEF(LWS_CALLBACK_FILTER_NETWORK_CONNECTION),
    CDEF(LWS_CALLBACK_WS_CLIENT_BIND_PROTOCOL),
    CDEF(LWS_CALLBACK_WS_CLIENT_DROP_PROTOCOL),
    CDEF(LWS_CALLBACK_GET_THREAD_ID),
    CDEF(LWS_CALLBACK_ADD_POLL_FD),
    CDEF(LWS_CALLBACK_DEL_POLL_FD),
    CDEF(LWS_CALLBACK_CHANGE_MODE_POLL_FD),
    CDEF(LWS_CALLBACK_LOCK_POLL),
    CDEF(LWS_CALLBACK_UNLOCK_POLL),
    CDEF(LWS_CALLBACK_CGI),
    CDEF(LWS_CALLBACK_CGI_TERMINATED),
    CDEF(LWS_CALLBACK_CGI_STDIN_DATA),
    CDEF(LWS_CALLBACK_CGI_STDIN_COMPLETED),
    CDEF(LWS_CALLBACK_CGI_PROCESS_ATTACH),
    CDEF(LWS_CALLBACK_SESSION_INFO),
    CDEF(LWS_CALLBACK_GS_EVENT),
    CDEF(LWS_CALLBACK_HTTP_PMO),
    CDEF(LWS_CALLBACK_RAW_RX),
    CDEF(LWS_CALLBACK_RAW_CLOSE),
    CDEF(LWS_CALLBACK_RAW_WRITEABLE),
    CDEF(LWS_CALLBACK_RAW_ADOPT),
    CDEF(LWS_CALLBACK_RAW_SKT_BIND_PROTOCOL),
    CDEF(LWS_CALLBACK_RAW_SKT_DROP_PROTOCOL),
    CDEF(LWS_CALLBACK_RAW_ADOPT_FILE),
    CDEF(LWS_CALLBACK_RAW_RX_FILE),
    CDEF(LWS_CALLBACK_RAW_WRITEABLE_FILE),
    CDEF(LWS_CALLBACK_RAW_CLOSE_FILE),
    CDEF(LWS_CALLBACK_RAW_FILE_BIND_PROTOCOL),
    CDEF(LWS_CALLBACK_RAW_FILE_DROP_PROTOCOL),
    CDEF(LWS_CALLBACK_TIMER),
    CDEF(LWS_CALLBACK_EVENT_WAIT_CANCELLED),
    CDEF(LWS_CALLBACK_CHILD_CLOSING),
    CDEF(LWS_CALLBACK_VHOST_CERT_AGING),
    CDEF(LWS_CALLBACK_VHOST_CERT_UPDATE),
    CDEF(LWS_CALLBACK_USER),
    CDEF(LWS_POLLHUP),
    CDEF(LWS_POLLIN),
    CDEF(LWS_POLLOUT),
    JS_CFUNC_DEF("decode_utf8", 1, js_decode_utf8),
    JS_CFUNC_DEF("parse_url", 1, js_lws_parse_url),
    JS_CFUNC_DEF("set_log_level", 1, js_lws_set_log_level),
    JS_CFUNC_DEF("create_context", 2, js_lws_create_context),
};

static const JSClassDef js_lws_context_class = {
    "Context",
    .finalizer = js_lws_context_finalizer,
    .gc_mark = js_lws_context_mark,
};

static const JSCFunctionListEntry js_lws_context_proto_funcs[] = {
    JS_CGETSET_DEF("connections", js_lws_context_get_connections, NULL),
    JS_CFUNC_DEF("client_connect", 8, js_lws_client_connect),
    JS_CFUNC_DEF("service_fd", 3, js_lws_service_fd),
    JS_CFUNC_DEF("service_periodic", 0, js_lws_service_periodic),
};

static const JSClassDef js_lws_wsi_class = {
    "WSI",
    .finalizer = js_lws_wsi_finalizer,
    .gc_mark = js_lws_wsi_mark,
};

#define HDRGET(name) JS_CGETSET_MAGIC_DEF(#name, js_lws_wsi_get_hdr, NULL, name)

static const JSCFunctionListEntry js_lws_wsi_proto_funcs[] = {
    JS_CGETSET_DEF("context", js_lws_wsi_get_context, NULL),
    JS_CGETSET_DEF("user", js_lws_wsi_get_user, NULL),
    HDRGET(WSI_TOKEN_GET_URI),
    HDRGET(WSI_TOKEN_POST_URI),
    HDRGET(WSI_TOKEN_OPTIONS_URI),
    HDRGET(WSI_TOKEN_HOST),
    HDRGET(WSI_TOKEN_CONNECTION),
    HDRGET(WSI_TOKEN_UPGRADE),
    HDRGET(WSI_TOKEN_ORIGIN),
    HDRGET(WSI_TOKEN_DRAFT),
    HDRGET(WSI_TOKEN_CHALLENGE),
    HDRGET(WSI_TOKEN_EXTENSIONS),
    HDRGET(WSI_TOKEN_KEY1),
    HDRGET(WSI_TOKEN_KEY2),
    HDRGET(WSI_TOKEN_PROTOCOL),
    HDRGET(WSI_TOKEN_ACCEPT),
    HDRGET(WSI_TOKEN_NONCE),
    HDRGET(WSI_TOKEN_HTTP),
    HDRGET(WSI_TOKEN_HTTP2_SETTINGS),
    HDRGET(WSI_TOKEN_HTTP_ACCEPT),
    HDRGET(WSI_TOKEN_HTTP_AC_REQUEST_HEADERS),
    HDRGET(WSI_TOKEN_HTTP_IF_MODIFIED_SINCE),
    HDRGET(WSI_TOKEN_HTTP_IF_NONE_MATCH),
    HDRGET(WSI_TOKEN_HTTP_ACCEPT_ENCODING),
    HDRGET(WSI_TOKEN_HTTP_ACCEPT_LANGUAGE),
    HDRGET(WSI_TOKEN_HTTP_PRAGMA),
    HDRGET(WSI_TOKEN_HTTP_CACHE_CONTROL),
    HDRGET(WSI_TOKEN_HTTP_AUTHORIZATION),
    HDRGET(WSI_TOKEN_HTTP_COOKIE),
    HDRGET(WSI_TOKEN_HTTP_CONTENT_LENGTH),
    HDRGET(WSI_TOKEN_HTTP_CONTENT_TYPE),
    HDRGET(WSI_TOKEN_HTTP_DATE),
    HDRGET(WSI_TOKEN_HTTP_RANGE),
    HDRGET(WSI_TOKEN_HTTP_REFERER),
    HDRGET(WSI_TOKEN_KEY),
    HDRGET(WSI_TOKEN_VERSION),
    HDRGET(WSI_TOKEN_SWORIGIN),
    HDRGET(WSI_TOKEN_HTTP_COLON_AUTHORITY),
    HDRGET(WSI_TOKEN_HTTP_COLON_METHOD),
    HDRGET(WSI_TOKEN_HTTP_COLON_PATH),
    HDRGET(WSI_TOKEN_HTTP_COLON_SCHEME),
    HDRGET(WSI_TOKEN_HTTP_COLON_STATUS),
    HDRGET(WSI_TOKEN_HTTP_ACCEPT_CHARSET),
    HDRGET(WSI_TOKEN_HTTP_ACCEPT_RANGES),
    HDRGET(WSI_TOKEN_HTTP_ACCESS_CONTROL_ALLOW_ORIGIN),
    HDRGET(WSI_TOKEN_HTTP_AGE),
    HDRGET(WSI_TOKEN_HTTP_ALLOW),
    HDRGET(WSI_TOKEN_HTTP_CONTENT_DISPOSITION),
    HDRGET(WSI_TOKEN_HTTP_CONTENT_ENCODING),
    HDRGET(WSI_TOKEN_HTTP_CONTENT_LANGUAGE),
    HDRGET(WSI_TOKEN_HTTP_CONTENT_LOCATION),
    HDRGET(WSI_TOKEN_HTTP_CONTENT_RANGE),
    HDRGET(WSI_TOKEN_HTTP_ETAG),
    HDRGET(WSI_TOKEN_HTTP_EXPECT),
    HDRGET(WSI_TOKEN_HTTP_EXPIRES),
    HDRGET(WSI_TOKEN_HTTP_FROM),
    HDRGET(WSI_TOKEN_HTTP_IF_MATCH),
    HDRGET(WSI_TOKEN_HTTP_IF_RANGE),
    HDRGET(WSI_TOKEN_HTTP_IF_UNMODIFIED_SINCE),
    HDRGET(WSI_TOKEN_HTTP_LAST_MODIFIED),
    HDRGET(WSI_TOKEN_HTTP_LINK),
    HDRGET(WSI_TOKEN_HTTP_LOCATION),
    HDRGET(WSI_TOKEN_HTTP_MAX_FORWARDS),
    HDRGET(WSI_TOKEN_HTTP_PROXY_AUTHENTICATE),
    HDRGET(WSI_TOKEN_HTTP_PROXY_AUTHORIZATION),
    HDRGET(WSI_TOKEN_HTTP_REFRESH),
    HDRGET(WSI_TOKEN_HTTP_RETRY_AFTER),
    HDRGET(WSI_TOKEN_HTTP_SERVER),
    HDRGET(WSI_TOKEN_HTTP_SET_COOKIE),
    HDRGET(WSI_TOKEN_HTTP_STRICT_TRANSPORT_SECURITY),
    HDRGET(WSI_TOKEN_HTTP_TRANSFER_ENCODING),
    HDRGET(WSI_TOKEN_HTTP_USER_AGENT),
    HDRGET(WSI_TOKEN_HTTP_VARY),
    HDRGET(WSI_TOKEN_HTTP_VIA),
    HDRGET(WSI_TOKEN_HTTP_WWW_AUTHENTICATE),
    HDRGET(WSI_TOKEN_PATCH_URI),
    HDRGET(WSI_TOKEN_PUT_URI),
    HDRGET(WSI_TOKEN_DELETE_URI),
    HDRGET(WSI_TOKEN_HTTP_URI_ARGS),
    HDRGET(WSI_TOKEN_PROXY),
    HDRGET(WSI_TOKEN_HTTP_X_REAL_IP),
    HDRGET(WSI_TOKEN_HTTP1_0),
    HDRGET(WSI_TOKEN_X_FORWARDED_FOR),
    HDRGET(WSI_TOKEN_CONNECT),
    HDRGET(WSI_TOKEN_HEAD_URI),
    HDRGET(WSI_TOKEN_TE),
    HDRGET(WSI_TOKEN_REPLAY_NONCE),
    HDRGET(WSI_TOKEN_COLON_PROTOCOL),
    HDRGET(WSI_TOKEN_X_AUTH_TOKEN),
    JS_CFUNC_DEF("is_final_fragment", 0, js_lws_is_final_fragment),
    JS_CFUNC_DEF("is_first_fragment", 0, js_lws_is_first_fragment),
    JS_CFUNC_DEF("frame_is_binary", 0, js_lws_frame_is_binary),
    JS_CFUNC_DEF("callback_on_writable", 0, js_lws_callback_on_writable),
    JS_CFUNC_DEF("write", 1, js_lws_write),
    JS_CFUNC_DEF("close_reason", 2, js_lws_close_reason),
};

static int js_lws_init(JSContext *ctx, JSModuleDef *m)
{
    JSValue proto;

    JS_NewClassID(&js_lws_context_class_id);
    JS_NewClass(JS_GetRuntime(ctx), js_lws_context_class_id,
                &js_lws_context_class);
    proto = JS_NewObject(ctx);
    JS_SetPropertyFunctionList(ctx, proto, js_lws_context_proto_funcs,
                               countof(js_lws_context_proto_funcs));
    JS_SetClassProto(ctx, js_lws_context_class_id, proto);

    JS_NewClassID(&js_lws_wsi_class_id);
    JS_NewClass(JS_GetRuntime(ctx), js_lws_wsi_class_id, &js_lws_wsi_class);
    proto = JS_NewObject(ctx);
    JS_SetPropertyFunctionList(ctx, proto, js_lws_wsi_proto_funcs,
                               countof(js_lws_wsi_proto_funcs));
    JS_SetClassProto(ctx, js_lws_wsi_class_id, proto);

    return JS_SetModuleExportList(ctx, m, js_lws_funcs, countof(js_lws_funcs));
}

JSModuleDef *js_init_module(JSContext *ctx, const char *module_name)
{
    JSModuleDef *m = JS_NewCModule(ctx, module_name, js_lws_init);
    if (m == NULL)
        return NULL;
    JS_AddModuleExportList(ctx, m, js_lws_funcs, countof(js_lws_funcs));
    return m;
}
