/*
 * js_value.h — Runtime value type for the blue JS-to-C compiler.
 *
 * Every JavaScript variable is represented as a JsValue — a tagged union
 * that can hold any JS primitive (undefined, null, boolean, number, string)
 * plus a property-bag object (JS_OBJECT) and a native function (JS_FUNCTION).
 *
 * The property bag (JsObject, up to JS_OBJ_MAX_PROPS entries) provides the
 * minimum object model needed to express `fs.readFileSync`, `path.join`, etc.
 * Property values are accessed via js_obj_get() and set via js_obj_set().
 * Native functions are stored as JsFnPtr values (JsValue(*)(int, JsValue*)).
 *
 * Include this header in any file generated by blue; it is self-contained
 * (C99, no external dependencies beyond the C standard library).
 *
 * Usage in generated code:
 *   JsValue x    = JS_NUM(5);
 *   JsValue name = JS_STR("world");
 *   JsValue flag = JS_BOOL(1);
 *   JsValue z    = js_add(name, JS_STR("!"));   // "world!"
 *   js_console_log(1, x);                        // prints "5"
 *   JsValue obj  = js_obj_new();                 // empty property bag
 *   js_obj_set(&obj, "key", JS_NUM(42));
 *   JsValue v    = js_obj_get(obj, JS_STR("key"));
 */

#ifndef JS_VALUE_H
#define JS_VALUE_H

/* Enable POSIX.1-2008 so that POSIX functions (realpath, getcwd, …) are
 * declared when system headers are first included.  This must appear before
 * any #include so glibc's feature-test machinery sees it. */
#ifndef _POSIX_C_SOURCE
#define _POSIX_C_SOURCE 200809L
#endif

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <limits.h>
#include <stdarg.h>
#include <stddef.h>

/* Portable strdup — strdup is POSIX, not C99. */
static inline char* js_strdup(const char* s) {
    if (!s) s = "";
    size_t len = strlen(s) + 1;
    char* p = (char*)malloc(len);
    if (p) memcpy(p, s, len);
    return p;
}

/* ── Value type tag ────────────────────────────────────────────────────────── */

typedef enum {
    JS_UNDEFINED = 0,
    JS_NULL,
    JS_BOOLEAN,
    JS_NUMBER,
    JS_STRING,
    JS_OBJECT,    /* property bag — pointer to heap-allocated JsObject */
    JS_FUNCTION,  /* native C function — stored as a JsFnPtr */
    JS_BYTES      /* raw byte buffer — Node-like Buffer MVP (see js_globals.h) */
} JsTag;

/* ── Forward declarations (needed for circular JsValue ↔ JsObject) ────────── */

typedef struct JsValue  JsValue;
typedef struct JsObject JsObject;
typedef struct JsBytes  JsBytes;

/* Heap-owned mutable byte blob (distinct from UTF-8 JS_STRING). */
struct JsBytes {
    unsigned char* data;
    size_t len;
};

/* Native function pointer: JsValue fn(int argc, JsValue* argv) */
typedef JsValue (*JsFnPtr)(int argc, JsValue* argv);

/* Forward declarations — defined after js_str() / js_obj_new(). */
static inline void js_dispose_value(JsValue* v);
static inline JsValue js_clone_value(JsValue src);
static inline JsValue js_num(double n);

/* ── JsValue struct ─────────────────────────────────────────────────────────── */

struct JsValue {
    JsTag tag;
    union {
        double    number;
        int       boolean;
        char*     string;   /* heap-allocated; freed by js_free() */
        JsObject* obj;      /* heap-allocated JsObject; not auto-freed (GC out of scope) */
        JsFnPtr   func;     /* native function pointer */
        JsBytes*  bytes;    /* owning pointer; data + blob freed via js_dispose_value */
    } as;
};

/* ── Property bag ────────────────────────────────────────────────────────────── */

#define JS_OBJ_MAX_PROPS 32
#define JS_OBJ_KEY_MAX   64

typedef struct {
    char    key[JS_OBJ_KEY_MAX];
    JsValue value;
} JsProp;

struct JsObject {
    JsProp props[JS_OBJ_MAX_PROPS];
    int    count;
    int    refcount; /* reference count — shared pointer semantics for objects */
};

/* Create an empty heap-allocated JsObject and wrap it in a JsValue. */
static inline JsValue js_obj_new(void) {
    JsValue v;
    v.tag    = JS_OBJECT;
    v.as.obj = (JsObject*)calloc(1, sizeof(JsObject));
    if (v.as.obj) v.as.obj->refcount = 1;
    return v;
}

/* Forward declaration — defined after js_obj_get_q where js_obj_new is available. */
static inline JsValue* js_fn_get_or_create_proto(JsFnPtr fn);

/* ── Closure variable store (for nested FDs capturing outer-scope vars) ─────
 * Maps a function pointer to a small array of captured JsValues.
 * The emitter sets captures at function-creation time and reads them at the
 * start of the function body.  Up to JS_CLOSURE_MAX_CAPS per function.     */
#define JS_CLOSURE_MAX_CAPS 8
#define JS_CLOSURE_FN_MAX   64
static struct {
    JsFnPtr fn;
    JsValue caps[JS_CLOSURE_MAX_CAPS];
    int     n;
} __jsc_closures[JS_CLOSURE_FN_MAX];
static int __jsc_closure_count;

static inline void js_closure_set(JsFnPtr fn, int ncaps, JsValue* caps) {
    if (!fn || ncaps <= 0) return;
    for (int i = 0; i < __jsc_closure_count; i++) {
        if (__jsc_closures[i].fn == fn) {
            for (int j = 0; j < __jsc_closures[i].n; j++)
                js_dispose_value(&__jsc_closures[i].caps[j]);
            __jsc_closures[i].n = 0;
            int m = ncaps < JS_CLOSURE_MAX_CAPS ? ncaps : JS_CLOSURE_MAX_CAPS;
            for (int j = 0; j < m; j++) __jsc_closures[i].caps[j] = js_clone_value(caps[j]);
            __jsc_closures[i].n = m;
            return;
        }
    }
    if (__jsc_closure_count < JS_CLOSURE_FN_MAX) {
        int idx = __jsc_closure_count++;
        __jsc_closures[idx].fn = fn;
        __jsc_closures[idx].n = 0;
        int m = ncaps < JS_CLOSURE_MAX_CAPS ? ncaps : JS_CLOSURE_MAX_CAPS;
        for (int j = 0; j < m; j++) __jsc_closures[idx].caps[j] = js_clone_value(caps[j]);
        __jsc_closures[idx].n = m;
    }
}
static inline JsValue js_closure_get(JsFnPtr fn, int idx) {
    if (!fn) return (JsValue){JS_UNDEFINED, {0}};
    for (int i = 0; i < __jsc_closure_count; i++)
        if (__jsc_closures[i].fn == fn && idx < __jsc_closures[i].n)
            return js_clone_value(__jsc_closures[i].caps[idx]);
    return (JsValue){JS_UNDEFINED, {0}};
}

/* Set a property on a JS_OBJECT value (insert or overwrite).
 * Also handles JS_FUNCTION: sets on the function's prototype object. */
static inline void js_obj_set(JsValue* v, const char* key, JsValue val) {
    if (!v || !key) return;
    /* Allow setting properties directly on a function's prototype */
    if (v->tag == JS_FUNCTION && v->as.func && strcmp(key, "prototype") != 0) {
        JsValue* p = js_fn_get_or_create_proto(v->as.func);
        if (p) js_obj_set(p, key, val);
        return;
    }
    if (v->tag != JS_OBJECT || !v->as.obj) return;
    JsObject* o = v->as.obj;
    /* Overwrite existing key */
    for (int i = 0; i < o->count; i++) {
        if (strncmp(o->props[i].key, key, JS_OBJ_KEY_MAX - 1) == 0) {
            js_dispose_value(&o->props[i].value);
            o->props[i].value = val;
            return;
        }
    }
    /* Insert new key */
    if (o->count < JS_OBJ_MAX_PROPS) {
        size_t key_len = strlen(key);
        if (key_len >= (size_t)JS_OBJ_KEY_MAX) key_len = (size_t)JS_OBJ_KEY_MAX - 1;
        memcpy(o->props[o->count].key, key, key_len);
        o->props[o->count].key[key_len] = '\0';
        o->props[o->count].value = val;
        o->count++;
    }
}

/* Convenience: store a native function as a property. */
static inline void js_obj_set_fn(JsValue* v, const char* key, JsFnPtr fn) {
    JsValue fv;
    fv.tag     = JS_FUNCTION;
    fv.as.func = fn;
    js_obj_set(v, key, fv);
}

static inline JsValue js_native_fn(JsFnPtr f) {
    JsValue v;
    v.tag     = JS_FUNCTION;
    v.as.func = f;
    return v;
}

static inline JsValue js_byte_buffer_own(unsigned char* data, size_t len) {
    JsValue out;
    out.tag = JS_BYTES;
    JsBytes* b = (JsBytes*)malloc(sizeof(JsBytes));
    if (!b) {
        out.tag = JS_UNDEFINED;
        out.as.number = 0;
        free(data);
        return out;
    }
    b->data = data;
    b->len  = len;
    out.as.bytes = b;
    return out;
}

/* ── Function prototype store (for AOT class/constructor patterns) ──────────
 * Maps JsFnPtr → a heap JsObject used as Function.prototype.
 * Accessed via Fn.prototype and written by _proto.method = fn patterns.   */
#define JS_FN_PROTO_MAX 64
static struct { JsFnPtr fn; JsValue proto; } __jsc_fn_protos[JS_FN_PROTO_MAX];
static int __jsc_fn_proto_n;

static inline JsValue* js_fn_get_or_create_proto(JsFnPtr fn) {
    if (!fn) return NULL;
    for (int i = 0; i < __jsc_fn_proto_n; i++)
        if (__jsc_fn_protos[i].fn == fn) return &__jsc_fn_protos[i].proto;
    if (__jsc_fn_proto_n < JS_FN_PROTO_MAX) {
        __jsc_fn_protos[__jsc_fn_proto_n].fn    = fn;
        __jsc_fn_protos[__jsc_fn_proto_n].proto = js_obj_new();
        return &__jsc_fn_protos[__jsc_fn_proto_n++].proto;
    }
    return NULL;
}

/* Get a property from JS_OBJECT (with __proto__ chain walk), JS_FUNCTION
 * (.prototype), or emulate .length / [i] on JS_BYTES.
 * Accepts JS_STRING keys or JS_NUMBER keys (converted to integer string). */
static inline JsValue js_obj_get_q(JsValue v, const char* key) {
    if (!key) return (JsValue){JS_UNDEFINED, {0}};

    /* Function.prototype access */
    if (v.tag == JS_FUNCTION && v.as.func && strcmp(key, "prototype") == 0) {
        JsValue* p = js_fn_get_or_create_proto(v.as.func);
        return p ? js_clone_value(*p) : (JsValue){JS_UNDEFINED, {0}};
    }

    if (v.tag == JS_BYTES && v.as.bytes) {
        JsBytes* b = v.as.bytes;
        if (strcmp(key, "length") == 0)
            return js_num((double)b->len);
        char* ep = NULL;
        long long idx = strtoll(key, &ep, 10);
        if (ep != key && *ep == '\0') {
            if (idx >= 0 && (size_t)idx < b->len)
                return js_num((double)b->data[idx]);
        }
    } else if (v.tag == JS_OBJECT && v.as.obj) {
        JsObject* o = v.as.obj;
        for (int i = 0; i < o->count; i++) {
            if (strcmp(o->props[i].key, key) == 0)
                return js_clone_value(o->props[i].value);
        }
        /* Walk __proto__ chain (one level) */
        if (strcmp(key, "__proto__") != 0) {
            for (int i = 0; i < o->count; i++) {
                if (strcmp(o->props[i].key, "__proto__") != 0) continue;
                JsValue proto = o->props[i].value;
                if (proto.tag == JS_OBJECT && proto.as.obj) {
                    JsObject* po = proto.as.obj;
                    for (int j = 0; j < po->count; j++)
                        if (strcmp(po->props[j].key, key) == 0)
                            return js_clone_value(po->props[j].value);
                }
                break;
            }
        }
    }
    return (JsValue){JS_UNDEFINED, {0}};
}

static inline JsValue js_obj_get(JsValue v, JsValue key) {
    char numkey[32];
    const char* kstr = NULL;
    if (key.tag == JS_STRING && key.as.string) {
        kstr = key.as.string;
    } else if (key.tag == JS_NUMBER) {
        long long ival = (long long)key.as.number;
        if ((double)ival == key.as.number)
            snprintf(numkey, sizeof(numkey), "%lld", ival);
        else
            snprintf(numkey, sizeof(numkey), "%g", key.as.number);
        kstr = numkey;
    }
    if (kstr) return js_obj_get_q(v, kstr);
    return (JsValue){JS_UNDEFINED, {0}};
}

/* Call a JS_FUNCTION value.  Returns JS_UNDEFINED if not callable.
 * Variadic args after 'argc' must be JsValue. */
static inline JsValue js_call(JsValue fn, int argc, ...) {
    if (fn.tag != JS_FUNCTION || !fn.as.func) return (JsValue){JS_UNDEFINED, {0}};
    JsValue argv[argc > 0 ? argc : 1];
    va_list ap;
    va_start(ap, argc);
    for (int i = 0; i < argc; i++)
        argv[i] = va_arg(ap, JsValue);
    va_end(ap);
    return fn.as.func(argc, argv);
}

static inline JsValue js_call_argv(JsValue fn, int argc, JsValue* argv) {
    if (fn.tag != JS_FUNCTION || !fn.as.func)
        return (JsValue){JS_UNDEFINED, {0}};
    JsValue tmp[JS_OBJ_MAX_PROPS];
    int n = (argc < JS_OBJ_MAX_PROPS) ? argc : JS_OBJ_MAX_PROPS;
    if (argc <= 0) return fn.as.func(0, tmp);
    if (!argv) return (JsValue){JS_UNDEFINED, {0}};
    for (int i = 0; i < n; i++) tmp[i] = argv[i];
    return fn.as.func(n, tmp);
}

/* ── Constructors ───────────────────────────────────────────────────────────── */

static inline JsValue js_undefined(void) {
    JsValue v; v.tag = JS_UNDEFINED; v.as.number = 0; return v;
}
static inline JsValue js_null(void) {
    JsValue v; v.tag = JS_NULL; v.as.number = 0; return v;
}
static inline JsValue js_bool(int b) {
    JsValue v; v.tag = JS_BOOLEAN; v.as.boolean = (b != 0); return v;
}
static inline JsValue js_num(double n) {
    JsValue v; v.tag = JS_NUMBER; v.as.number = n; return v;
}
static inline JsValue js_str(const char* s) {
    JsValue v;
    v.tag = JS_STRING;
    v.as.string = s ? js_strdup(s) : js_strdup("");
    return v;
}

/* Convenience macros so generated code reads like JS */
#define JS_UNDEFINED()  js_undefined()
#define JS_NULL()       js_null()
#define JS_BOOL(b)      js_bool(b)
#define JS_NUM(n)       js_num((double)(n))
#define JS_STR(s)       js_str(s)

/* ── Deep clone / dispose (used by C++ JsOwned and by js_obj_get) ───────────── */

static inline JsValue js_clone_value(JsValue src) {
    switch (src.tag) {
        case JS_UNDEFINED:
        case JS_NULL:
        case JS_BOOLEAN:
        case JS_NUMBER:
        case JS_FUNCTION:
            return src;
        case JS_STRING:
            return js_str(src.as.string ? src.as.string : "");
        case JS_BYTES: {
            if (!src.as.bytes) return (JsValue){JS_UNDEFINED, {0}};
            size_t len = src.as.bytes->len;
            unsigned char* cpy = (unsigned char*)malloc(len ? len : 1);
            if (!cpy) return (JsValue){JS_UNDEFINED, {0}};
            if (len && src.as.bytes->data) memcpy(cpy, src.as.bytes->data, len);
            else cpy[0] = '\0';
            return js_byte_buffer_own(cpy, len);
        }
        case JS_OBJECT: {
            /* Shallow copy: share the JsObject pointer, increment refcount. */
            if (!src.as.obj) return (JsValue){JS_UNDEFINED, {0}};
            src.as.obj->refcount++;
            return src;
        }
    }
    return (JsValue){JS_UNDEFINED, {0}};
}

static inline void js_dispose_value(JsValue* v) {
    if (!v) return;
    if (v->tag == JS_BYTES && v->as.bytes) {
        free(v->as.bytes->data);
        free(v->as.bytes);
        v->as.bytes = NULL;
        v->tag = JS_UNDEFINED;
        v->as.number = 0;
        return;
    }
    if (v->tag == JS_STRING && v->as.string) {
        free(v->as.string);
        v->as.string = NULL;
    } else if (v->tag == JS_OBJECT && v->as.obj) {
        JsObject* o = v->as.obj;
        if (--o->refcount <= 0) {
            for (int i = 0; i < o->count; i++)
                js_dispose_value(&o->props[i].value);
            free(o);
        }
        v->as.obj = NULL;
    }
    v->tag = JS_UNDEFINED;
    v->as.number = 0;
}

/* ── Memory ─────────────────────────────────────────────────────────────────── */

/* Free heap string inside a JsValue (call before reassigning a string var). */
static inline void js_free(JsValue* v) {
    if (!v) return;
    if (v->tag == JS_STRING && v->as.string) {
        free(v->as.string);
        v->as.string = NULL;
    }
    /* JS_OBJECT: the JsObject itself is not freed here (GC is out of scope).
     * String values inside the bag are similarly not recursively freed.
     * For the current use case (short-lived module objects), this is acceptable. */
}

/* ── Coercions ──────────────────────────────────────────────────────────────── */

/* JS truthiness: false, 0, "", null, undefined → 0; everything else → 1 */
static inline int js_truthy(JsValue v) {
    switch (v.tag) {
        case JS_UNDEFINED: return 0;
        case JS_NULL:      return 0;
        case JS_BOOLEAN:   return v.as.boolean;
        case JS_NUMBER:    return v.as.number != 0.0 && !isnan(v.as.number);
        case JS_STRING:    return v.as.string && v.as.string[0] != '\0';
        case JS_OBJECT:    return v.as.obj != NULL;
        case JS_FUNCTION:  return v.as.func != NULL;
        case JS_BYTES:     return v.as.bytes != NULL && v.as.bytes->len > 0;
    }
    return 0;
}

/* Convert any JsValue to its numeric representation (like Number() in JS) */
static inline double js_to_num(JsValue v) {
    switch (v.tag) {
        case JS_UNDEFINED: return NAN;
        case JS_NULL:      return 0.0;
        case JS_BOOLEAN:   return (double)v.as.boolean;
        case JS_NUMBER:    return v.as.number;
        case JS_STRING:    return v.as.string ? atof(v.as.string) : 0.0;
        case JS_OBJECT:    return NAN;
        case JS_FUNCTION:  return NAN;
        case JS_BYTES:     return (double)(v.as.bytes ? v.as.bytes->len : 0);
    }
    return NAN;
}

static inline int js_is_number(JsValue v) {
    return v.tag == JS_NUMBER;
}

/* Convert any JsValue to a heap-allocated C string (caller must free). */
static inline char* js_to_cstr(JsValue v) {
    char buf[64];
    switch (v.tag) {
        case JS_UNDEFINED: return js_strdup("undefined");
        case JS_NULL:      return js_strdup("null");
        case JS_BOOLEAN:   return js_strdup(v.as.boolean ? "true" : "false");
        case JS_STRING:    return js_strdup(v.as.string ? v.as.string : "");
        case JS_OBJECT:    return js_strdup("[object Object]");
        case JS_FUNCTION:  return js_strdup("[Function]");
        case JS_BYTES: {
            size_t len = v.as.bytes ? v.as.bytes->len : 0;
            char* s = (char*)malloc(len + 1);
            if (!s) return js_strdup("");
            if (len && v.as.bytes->data) memcpy(s, v.as.bytes->data, len);
            s[len] = '\0';
            return s; /* latin-1-ish; binary may truncate at \\0 printed */
        }
        case JS_NUMBER:
            /* Mimic JS number formatting: integers without decimal point */
            if (v.as.number == (long long)v.as.number &&
                !isinf(v.as.number) && !isnan(v.as.number)) {
                snprintf(buf, sizeof(buf), "%lld", (long long)v.as.number);
            } else if (isnan(v.as.number)) {
                return js_strdup("NaN");
            } else if (isinf(v.as.number)) {
                return js_strdup(v.as.number > 0 ? "Infinity" : "-Infinity");
            } else {
                snprintf(buf, sizeof(buf), "%g", v.as.number);
            }
            return js_strdup(buf);
    }
    return js_strdup("");
}

/* ── Arithmetic operators ────────────────────────────────────────────────────── */

/* js_add: numeric addition OR string concatenation (mirrors JS + semantics) */
static inline JsValue js_add(JsValue a, JsValue b) {
    if (a.tag == JS_STRING || b.tag == JS_STRING) {
        char* sa = js_to_cstr(a);
        char* sb = js_to_cstr(b);
        size_t len = strlen(sa) + strlen(sb) + 1;
        char* result = (char*)malloc(len);
        if (result) {
            strcpy(result, sa);
            strcat(result, sb);
        }
        free(sa);
        free(sb);
        JsValue v; v.tag = JS_STRING; v.as.string = result;
        return v;
    }
    return js_num(js_to_num(a) + js_to_num(b));
}

static inline JsValue js_sub(JsValue a, JsValue b) {
    return js_num(js_to_num(a) - js_to_num(b));
}
static inline JsValue js_mul(JsValue a, JsValue b) {
    return js_num(js_to_num(a) * js_to_num(b));
}
static inline JsValue js_div(JsValue a, JsValue b) {
    double da = js_to_num(a);
    double db = js_to_num(b);
    return js_num(da / db);
}
static inline JsValue js_mod(JsValue a, JsValue b) {
    return js_num(fmod(js_to_num(a), js_to_num(b)));
}
static inline JsValue js_pow(JsValue a, JsValue b) {
    return js_num(pow(js_to_num(a), js_to_num(b)));
}

/* ── Comparison operators (return JsValue boolean) ─────────────────────────── */

static inline JsValue js_eq(JsValue a, JsValue b) {
    if (a.tag != b.tag) return js_bool(0);
    if (a.tag == JS_NUMBER) return js_bool(a.as.number == b.as.number);
    if (a.tag == JS_STRING) return js_bool(strcmp(a.as.string, b.as.string) == 0);
    if (a.tag == JS_BOOLEAN) return js_bool(a.as.boolean == b.as.boolean);
    if (a.tag == JS_NULL || a.tag == JS_UNDEFINED) return js_bool(1);
    if (a.tag == JS_BYTES && a.as.bytes && b.as.bytes && a.as.bytes->len == b.as.bytes->len)
        return js_bool(memcmp(a.as.bytes->data, b.as.bytes->data, a.as.bytes->len) == 0);
    return js_bool(0);
}
static inline JsValue js_neq(JsValue a, JsValue b) {
    return js_bool(!js_truthy(js_eq(a, b)));
}
static inline JsValue js_lt(JsValue a, JsValue b) {
    if (a.tag == JS_STRING && b.tag == JS_STRING)
        return js_bool(strcmp(a.as.string, b.as.string) < 0);
    if (js_is_number(a) && js_is_number(b))
        return js_bool(a.as.number < b.as.number);
    return js_bool(js_to_num(a) < js_to_num(b));
}
static inline JsValue js_lte(JsValue a, JsValue b) {
    if (a.tag == JS_STRING && b.tag == JS_STRING)
        return js_bool(strcmp(a.as.string, b.as.string) <= 0);
    if (js_is_number(a) && js_is_number(b))
        return js_bool(a.as.number <= b.as.number);
    return js_bool(js_to_num(a) <= js_to_num(b));
}
static inline JsValue js_gt(JsValue a, JsValue b)  { return js_lt(b, a); }
static inline JsValue js_gte(JsValue a, JsValue b) { return js_lte(b, a); }

/* ── Logical operators ───────────────────────────────────────────────────────── */

/* JS &&: returns the first falsy value, or the last value if all truthy */
static inline JsValue js_and(JsValue a, JsValue b) {
    return js_clone_value(js_truthy(a) ? b : a);
}
/* JS ||: returns the first truthy value, or the last value if all falsy */
static inline JsValue js_or(JsValue a, JsValue b) {
    return js_clone_value(js_truthy(a) ? a : b);
}
static inline JsValue js_not(JsValue a) {
    return js_bool(!js_truthy(a));
}

/* ── Unary operators ─────────────────────────────────────────────────────────── */

static inline JsValue js_neg(JsValue a)    { return js_num(-js_to_num(a)); }
static inline JsValue js_typeof(JsValue a) {
    switch (a.tag) {
        case JS_UNDEFINED: return js_str("undefined");
        case JS_NULL:      return js_str("object");   /* JS quirk */
        case JS_BOOLEAN:   return js_str("boolean");
        case JS_NUMBER:    return js_str("number");
        case JS_STRING:    return js_str("string");
        case JS_OBJECT:    return js_str("object");
        case JS_FUNCTION:  return js_str("function");
        case JS_BYTES:     return js_str("object"); /* duck-type like Buffer */
    }
    return js_str("unknown");
}

/* ── console.log ─────────────────────────────────────────────────────────────── */

/* Print n JsValues separated by spaces, followed by a newline. */
static inline void js_console_log(int n, ...) {
    va_list ap;
    va_start(ap, n);
    for (int i = 0; i < n; i++) {
        JsValue v = va_arg(ap, JsValue);
        char* s = js_to_cstr(v);
        printf("%s%s", s, (i == n - 1) ? "" : " ");
        free(s);
    }
    fputc('\n', stdout);
    fflush(stdout);
    va_end(ap);
}

static inline void js_console_error(int n, ...) {
    va_list ap;
    va_start(ap, n);
    for (int i = 0; i < n; i++) {
        JsValue v = va_arg(ap, JsValue);
        char* s = js_to_cstr(v);
        fprintf(stderr, "%s%s", s, (i == n - 1) ? "" : " ");
        free(s);
    }
    fputc('\n', stderr);
    fflush(stderr);
    va_end(ap);
}

/* ── Update expressions (++/--) ──────────────────────────────────────────────── */

/* Pre-increment: ++v  (modifies in place, returns new value) */
static inline JsValue js_preinc(JsValue* v) {
    v->tag = JS_NUMBER;
    v->as.number = js_to_num(*v) + 1.0;
    return *v;
}
static inline JsValue js_predec(JsValue* v) {
    v->tag = JS_NUMBER;
    v->as.number = js_to_num(*v) - 1.0;
    return *v;
}
/* Post-increment: v++  (returns old value, modifies in place) */
static inline JsValue js_postinc(JsValue* v) {
    JsValue old = *v;
    v->tag = JS_NUMBER;
    v->as.number = js_to_num(*v) + 1.0;
    return old;
}
static inline JsValue js_postdec(JsValue* v) {
    JsValue old = *v;
    v->tag = JS_NUMBER;
    v->as.number = js_to_num(*v) - 1.0;
    return old;
}

/* ── Nullish coalescing: a ?? b ─────────────────────────────────────────────── */
static inline JsValue js_nullish_coalesce(JsValue a, JsValue b) {
    return js_clone_value((a.tag == JS_UNDEFINED || a.tag == JS_NULL) ? b : a);
}

/* ── Object key iteration (for AOT for-in loops) ────────────────────────────── */
static inline int js_obj_key_count(JsValue v) {
    if (v.tag != JS_OBJECT || !v.as.obj) return 0;
    return v.as.obj->count;
}
static inline JsValue js_obj_key_at(JsValue v, int i) {
    if (v.tag != JS_OBJECT || !v.as.obj || i < 0 || i >= v.as.obj->count)
        return js_undefined();
    return js_str(v.as.obj->props[i].key);
}

/* ── CommonJS require() built-in stub ───────────────────────────────────────
 * Stub that returns JS_UNDEFINED for all built-in module names.
 * When js_node.h is included, js_node_require() should be used instead;
 * the emitter generates calls to js_node_require() for known builtins.
 */
static inline JsValue js_require_builtin(JsValue name) {
    (void)name;
    return JS_UNDEFINED();
}

/* ── window.open() ───────────────────────────────────────────────────────────
 * Allows JS code compiled by blue to open a native window:
 *   window.open("<h1>Hello</h1>", "Title", 800, 600);
 *
 * The driver links the platform WebView TU when this call site is present
 * (see #define JSC_RUNTIME_WEBVIEW in generated code).
 */
#include "js_window.h"

static inline JsValue js_window_open(JsValue html, JsValue title,
                                      JsValue w, JsValue h) {
    char* html_s  = js_to_cstr(html);
    char* title_s = js_to_cstr(title);
    int   wi      = (int)js_to_num(w);
    int   hi      = (int)js_to_num(h);
    window_open(title_s, html_s, wi, hi);
    free(html_s);
    free(title_s);
    return JS_UNDEFINED();
}

#endif /* JS_VALUE_H */
