// json_playdate.c - Cell integration for Playdate JSON API
// Wraps pd_api_json.h functions for JavaScript access

#include "cell.h"
#include "common.h"
#include <string.h>
#include <stdlib.h>

// --- JSON Decoder Context ---
typedef struct {
    JSContext *js;
    JSValue result;
    JSValue stack[32];
    int stack_depth;
} JsonDecodeCtx;

static void json_decode_error(json_decoder *decoder, const char *error, int linenum) {
    JsonDecodeCtx *ctx = (JsonDecodeCtx*)decoder->userdata;
    ctx->result = JS_ThrowSyntaxError(ctx->js, "JSON parse error at line %d: %s", linenum, error);
}

static void json_decode_will_sublist(json_decoder *decoder, const char *name, json_value_type type) {
    JsonDecodeCtx *ctx = (JsonDecodeCtx*)decoder->userdata;
    JSValue container = (type == kJSONArray) ? JS_NewArray(ctx->js) : JS_NewObject(ctx->js);
    if (ctx->stack_depth < 32) {
        ctx->stack[ctx->stack_depth++] = container;
    }
}

static void* json_decode_did_sublist(json_decoder *decoder, const char *name, json_value_type type) {
    JsonDecodeCtx *ctx = (JsonDecodeCtx*)decoder->userdata;
    if (ctx->stack_depth > 0) {
        ctx->stack_depth--;
        JSValue completed = ctx->stack[ctx->stack_depth];
        if (ctx->stack_depth == 0) {
            ctx->result = completed;
        }
        return NULL;
    }
    return NULL;
}

static void json_decode_table_value(json_decoder *decoder, const char *key, json_value value) {
    JsonDecodeCtx *ctx = (JsonDecodeCtx*)decoder->userdata;
    if (ctx->stack_depth <= 0) return;
    JSValue container = ctx->stack[ctx->stack_depth - 1];
    JSValue jsval;
    switch (value.type) {
        case kJSONNull: jsval = JS_NULL; break;
        case kJSONTrue: jsval = JS_TRUE; break;
        case kJSONFalse: jsval = JS_FALSE; break;
        case kJSONInteger: jsval = JS_NewInt32(ctx->js, value.data.intval); break;
        case kJSONFloat: jsval = JS_NewFloat64(ctx->js, value.data.floatval); break;
        case kJSONString: jsval = JS_NewString(ctx->js, value.data.stringval); break;
        case kJSONArray:
        case kJSONTable:
            if (ctx->stack_depth > 1) {
                jsval = ctx->stack[ctx->stack_depth - 1];
            } else {
                jsval = JS_NULL;
            }
            break;
        default: jsval = JS_NULL; break;
    }
    JS_SetPropertyStr(ctx->js, container, key, jsval);
}

static void json_decode_array_value(json_decoder *decoder, int pos, json_value value) {
    JsonDecodeCtx *ctx = (JsonDecodeCtx*)decoder->userdata;
    if (ctx->stack_depth <= 0) return;
    JSValue container = ctx->stack[ctx->stack_depth - 1];
    JSValue jsval;
    switch (value.type) {
        case kJSONNull: jsval = JS_NULL; break;
        case kJSONTrue: jsval = JS_TRUE; break;
        case kJSONFalse: jsval = JS_FALSE; break;
        case kJSONInteger: jsval = JS_NewInt32(ctx->js, value.data.intval); break;
        case kJSONFloat: jsval = JS_NewFloat64(ctx->js, value.data.floatval); break;
        case kJSONString: jsval = JS_NewString(ctx->js, value.data.stringval); break;
        default: jsval = JS_NULL; break;
    }
    JS_SetPropertyNumber(ctx->js, container, pos, jsval);
}

// --- JSON Encoder Context ---
typedef struct {
    char *buffer;
    size_t len;
    size_t cap;
} JsonEncodeCtx;

static void json_encode_write(void *userdata, const char *str, int len) {
    JsonEncodeCtx *ctx = (JsonEncodeCtx*)userdata;
    if (ctx->len + len + 1 > ctx->cap) {
        ctx->cap = (ctx->cap + len + 1) * 2;
        ctx->buffer = realloc(ctx->buffer, ctx->cap);
    }
    memcpy(ctx->buffer + ctx->len, str, len);
    ctx->len += len;
    ctx->buffer[ctx->len] = '\0';
}

static void encode_js_value(json_encoder *enc, JSContext *js, JSValue val);

static void encode_js_object(json_encoder *enc, JSContext *js, JSValue obj) {
    enc->startTable(enc);
    JSPropertyEnum *props;
    uint32_t len;
    if (JS_GetOwnPropertyNames(js, &props, &len, obj, JS_GPN_STRING_MASK | JS_GPN_ENUM_ONLY) == 0) {
        for (uint32_t i = 0; i < len; i++) {
            const char *key = JS_AtomToCString(js, props[i].atom);
            JSValue val = JS_GetProperty(js, obj, props[i].atom);
            enc->addTableMember(enc, key, strlen(key));
            encode_js_value(enc, js, val);
            JS_FreeValue(js, val);
            JS_FreeCString(js, key);
            JS_FreeAtom(js, props[i].atom);
        }
        js_free(js, props);
    }
    enc->endTable(enc);
}

static void encode_js_array(json_encoder *enc, JSContext *js, JSValue arr) {
    enc->startArray(enc);
    JSValue lenVal = JS_GetPropertyStr(js, arr, "length");
    int len = (int)js2number(js, lenVal);
    JS_FreeValue(js, lenVal);
    for (int i = 0; i < len; i++) {
        enc->addArrayMember(enc);
        JSValue val = JS_GetPropertyNumber(js, arr, i);
        encode_js_value(enc, js, val);
        JS_FreeValue(js, val);
    }
    enc->endArray(enc);
}

static void encode_js_value(json_encoder *enc, JSContext *js, JSValue val) {
    if (JS_IsNull(val)) {
        enc->writeNull(enc);
    } else if (JS_IsBool(val)) {
        if (JS_ToBool(js, val)) enc->writeTrue(enc);
        else enc->writeFalse(enc);
    } else if (JS_IsNumber(val)) {
        double d = js2number(js, val);
        if (d == (int)d) enc->writeInt(enc, (int)d);
        else enc->writeDouble(enc, d);
    } else if (JS_IsText(val)) {
        size_t len;
        const char *str = JS_ToCStringLen(js, &len, val);
        enc->writeString(enc, str, len);
        JS_FreeCString(js, str);
    } else if (JS_IsArray(val)) {
        encode_js_array(enc, js, val);
    } else if (JS_IsObject(val)) {
        encode_js_object(enc, js, val);
    } else {
        enc->writeNull(enc);
    }
}

// --- API Functions ---

JSC_SCALL(json_decodeString,
    if (!pd_json) return JS_ThrowInternalError(js, "json not initialized");
    JsonDecodeCtx ctx = { js, JS_NULL, {}, 0 };
    json_decoder decoder = {0};
    decoder.decodeError = json_decode_error;
    decoder.willDecodeSublist = json_decode_will_sublist;
    decoder.didDecodeSublist = json_decode_did_sublist;
    decoder.didDecodeTableValue = json_decode_table_value;
    decoder.didDecodeArrayValue = json_decode_array_value;
    decoder.userdata = &ctx;
    json_value outval;
    pd_json->decodeString(&decoder, str, &outval);
    ret = ctx.result;
)

JSC_CCALL(json_encode,
    if (!pd_json) return JS_ThrowInternalError(js, "json not initialized");
    JsonEncodeCtx ctx = { malloc(256), 0, 256 };
    if (!ctx.buffer) return JS_ThrowInternalError(js, "malloc failed");
    ctx.buffer[0] = '\0';
    json_encoder enc;
    int pretty = argc > 1 ? JS_ToBool(js, argv[1]) : 0;
    pd_json->initEncoder(&enc, json_encode_write, &ctx, pretty);
    encode_js_value(&enc, js, argv[0]);
    JSValue result = JS_NewStringLen(js, ctx.buffer, ctx.len);
    free(ctx.buffer);
    return result;
)

static const JSCFunctionListEntry js_json_funcs[] = {
    MIST_FUNC_DEF(json, decodeString, 1),
    MIST_FUNC_DEF(json, encode, 2),
};

JSValue js_json_use(JSContext *js) {
    JSValue mod = JS_NewObject(js);
    JS_SetPropertyFunctionList(js, mod, js_json_funcs, countof(js_json_funcs));
    return mod;
}
