#define WOTA_IMPLEMENTATION
#include "quickjs-internal.h"
#include "cell.h"

typedef struct ObjectRef {
  void *ptr;
  struct ObjectRef *next;
} ObjectRef;

typedef struct WotaEncodeContext {
  JSContext *ctx;
  ObjectRef *visited_stack;
  WotaBuffer wb;
  int cycle;
  JSValue replacer;
} WotaEncodeContext;

static void wota_stack_push (WotaEncodeContext *enc, JSValueConst val) {
  (void)enc; (void)val;
  /* Cycle detection disabled for performance */
}

static void wota_stack_pop (WotaEncodeContext *enc) {
  if (!enc->visited_stack) return;

  ObjectRef *top = enc->visited_stack;
  enc->visited_stack = top->next;
  sys_free (top);
}

static int wota_stack_has (WotaEncodeContext *enc, JSValueConst val) {
  (void)enc; (void)val;
  return 0;
  /* Cycle detection disabled for performance */
}

static void wota_stack_free (WotaEncodeContext *enc) {
  while (enc->visited_stack) {
    wota_stack_pop (enc);
  }
}

static JSValue wota_apply_replacer (WotaEncodeContext *enc, JSValueConst holder, JSValue key, JSValueConst val) {
  if (JS_IsNull (enc->replacer)) return JS_DupValue (enc->ctx, val);
  JSValue key_val = JS_IsNull (key) ? JS_NULL : JS_DupValue (enc->ctx, key);
  JSValue args[2] = { key_val, JS_DupValue (enc->ctx, val) };
  JSValue result = JS_Call (enc->ctx, enc->replacer, holder, 2, args);
  JS_FreeValue (enc->ctx, args[0]);
  JS_FreeValue (enc->ctx, args[1]);
  if (JS_IsException (result)) return JS_DupValue (enc->ctx, val);
  return result;
}

static void wota_encode_value (WotaEncodeContext *enc, JSValueConst val, JSValueConst holder, JSValue key);

static void encode_object_properties (WotaEncodeContext *enc, JSValueConst val, JSValueConst holder) {
  JSContext *ctx = enc->ctx;

  /* Root the input value to protect it during property enumeration */
  JSGCRef val_ref, keys_ref;
  JS_PushGCRef (ctx, &val_ref);
  JS_PushGCRef (ctx, &keys_ref);
  val_ref.val = JS_DupValue (ctx, val);

  keys_ref.val = JS_GetOwnPropertyNames (ctx, val_ref.val);
  if (JS_IsException (keys_ref.val)) {
    wota_write_sym (&enc->wb, WOTA_NULL);
    JS_FreeValue (ctx, val_ref.val);
    JS_PopGCRef (ctx, &keys_ref);
    JS_PopGCRef (ctx, &val_ref);
    return;
  }
  int64_t plen64;
  if (JS_GetLength (ctx, keys_ref.val, &plen64) < 0) {
    JS_FreeValue (ctx, keys_ref.val);
    JS_FreeValue (ctx, val_ref.val);
    wota_write_sym (&enc->wb, WOTA_NULL);
    JS_PopGCRef (ctx, &keys_ref);
    JS_PopGCRef (ctx, &val_ref);
    return;
  }
  uint32_t plen = (uint32_t)plen64;
  uint32_t non_function_count = 0;

  /* Allocate GC-rooted arrays for props and keys */
  JSGCRef *prop_refs = sys_malloc (sizeof (JSGCRef) * plen);
  JSGCRef *key_refs = sys_malloc (sizeof (JSGCRef) * plen);
  for (uint32_t i = 0; i < plen; i++) {
    JS_PushGCRef (ctx, &prop_refs[i]);
    JS_PushGCRef (ctx, &key_refs[i]);
    prop_refs[i].val = JS_NULL;
    key_refs[i].val = JS_NULL;
  }

  for (uint32_t i = 0; i < plen; i++) {
    /* Store key into its GCRef slot immediately so it's rooted before
       JS_GetProperty can trigger GC and relocate the string. */
    key_refs[i].val = JS_GetPropertyNumber (ctx, keys_ref.val, i);
    JSValue prop_val = JS_GetProperty (ctx, val_ref.val, key_refs[i].val);
    if (!JS_IsFunction (prop_val)) {
      if (i != non_function_count) {
        key_refs[non_function_count].val = key_refs[i].val;
        key_refs[i].val = JS_NULL;
      }
      prop_refs[non_function_count].val = prop_val;
      non_function_count++;
    } else {
      JS_FreeValue (ctx, prop_val);
      JS_FreeValue (ctx, key_refs[i].val);
      key_refs[i].val = JS_NULL;
    }
  }
  JS_FreeValue (ctx, keys_ref.val);
  wota_write_record (&enc->wb, non_function_count);
  for (uint32_t i = 0; i < non_function_count; i++) {
    size_t klen;
    const char *prop_name = JS_ToCStringLen (ctx, &klen, key_refs[i].val);
    wota_write_text_len (&enc->wb, prop_name ? prop_name : "", prop_name ? klen : 0);
    wota_encode_value (enc, prop_refs[i].val, val_ref.val, key_refs[i].val);
    JS_FreeCString (ctx, prop_name);
    JS_FreeValue (ctx, prop_refs[i].val);
    JS_FreeValue (ctx, key_refs[i].val);
  }
  /* Pop all GC refs in reverse order */
  for (int i = plen - 1; i >= 0; i--) {
    JS_PopGCRef (ctx, &key_refs[i]);
    JS_PopGCRef (ctx, &prop_refs[i]);
  }
  sys_free (prop_refs);
  sys_free (key_refs);
  JS_FreeValue (ctx, val_ref.val);
  JS_PopGCRef (ctx, &keys_ref);
  JS_PopGCRef (ctx, &val_ref);
}

static void wota_encode_value (WotaEncodeContext *enc, JSValueConst val, JSValueConst holder, JSValue key) {
  JSContext *ctx = enc->ctx;
  JSValue replaced;
  if (!JS_IsNull (enc->replacer) && !JS_IsNull (key))
    replaced = wota_apply_replacer (enc, holder, key, val);
  else
    replaced = JS_DupValue (enc->ctx, val);

  int tag = JS_VALUE_GET_TAG (replaced);
  switch (tag) {
    case JS_TAG_INT: {
      int32_t d;
      JS_ToInt32 (ctx, &d, replaced);
      wota_write_int_word (&enc->wb, d);
      break;
    }
    case JS_TAG_FLOAT64: {
      double d;
      if (JS_ToFloat64 (ctx, &d, replaced) < 0) {
        wota_write_sym (&enc->wb, WOTA_NULL);
        break;
      }
      wota_write_float_word (&enc->wb, d);
      break;
    }
    case JS_TAG_STRING: {
      size_t plen;
      const char *str = JS_ToCStringLen (ctx, &plen, replaced);
      wota_write_text_len (&enc->wb, str ? str : "", str ? plen : 0);
      JS_FreeCString (ctx, str);
      break;
    }
    case JS_TAG_BOOL:
      wota_write_sym (&enc->wb, JS_VALUE_GET_BOOL (replaced) ? WOTA_TRUE : WOTA_FALSE);
      break;
    case JS_TAG_NULL:
      wota_write_sym (&enc->wb, WOTA_NULL);
      break;
    case JS_TAG_PTR: {
      if (JS_IsText (replaced)) {
        size_t plen;
        const char *str = JS_ToCStringLen (ctx, &plen, replaced);
        wota_write_text_len (&enc->wb, str ? str : "", str ? plen : 0);
        JS_FreeCString (ctx, str);
        break;
      }
      if (js_is_blob (ctx, replaced)) {
        size_t buf_len;
        void *buf_data = js_get_blob_data (ctx, &buf_len, replaced);
        if (buf_data == (void *)-1) {
          JS_FreeValue (ctx, replaced);
          return;
        }
        if (buf_len == 0) {
          wota_write_blob (&enc->wb, 0, "");
        } else {
          wota_write_blob (&enc->wb, (unsigned long long)buf_len * 8, (const char *)buf_data);
        }
        break;
      }
      if (JS_IsArray (replaced)) {
        if (wota_stack_has (enc, replaced)) {
          enc->cycle = 1;
          break;
        }
        wota_stack_push (enc, replaced);
        int64_t arr_len;
        JS_GetLength (ctx, replaced, &arr_len);
        wota_write_array (&enc->wb, arr_len);
        for (int64_t i = 0; i < arr_len; i++) {
          JSValue elem_val = JS_GetPropertyNumber (ctx, replaced, i);
          wota_encode_value (enc, elem_val, replaced, JS_NewInt32 (ctx, (int32_t)i));
          JS_FreeValue (ctx, elem_val);
        }
        wota_stack_pop (enc);
        break;
      }
      JSValue adata = JS_NULL;
      if (!JS_IsNull (ctx->actor_sym)) {
        int has = JS_HasPropertyKey (ctx, replaced, ctx->actor_sym);
        if (has > 0) adata = JS_GetPropertyKey (ctx, replaced, ctx->actor_sym);
      }
      if (!JS_IsNull (adata)) {
        wota_write_sym (&enc->wb, WOTA_PRIVATE);
        wota_encode_value (enc, adata, replaced, JS_NULL);
        JS_FreeValue (ctx, adata);
        break;
      }
      JS_FreeValue (ctx, adata);
      if (wota_stack_has (enc, replaced)) {
        enc->cycle = 1;
        break;
      }
      wota_stack_push (enc, replaced);
      JSValue to_json = JS_GetPropertyStr (ctx, replaced, "toJSON");
      if (JS_IsFunction (to_json)) {
        JSValue result = JS_Call (ctx, to_json, replaced, 0, NULL);
        JS_FreeValue (ctx, to_json);
        if (!JS_IsException (result)) {
          wota_encode_value (enc, result, holder, key);
          JS_FreeValue (ctx, result);
        } else
          wota_write_sym (&enc->wb, WOTA_NULL);
        wota_stack_pop (enc);
        break;
      }
      JS_FreeValue (ctx, to_json);
      encode_object_properties (enc, replaced, holder);
      wota_stack_pop (enc);
      break;
    }
    default:
      wota_write_sym (&enc->wb, WOTA_NULL);
      break;
  }
  JS_FreeValue (ctx, replaced);
}

static char *decode_wota_value (JSContext *ctx, char *data_ptr, JSValue *out_val, JSValue holder, JSValue key, JSValue reviver) {
  uint64_t first_word = *(uint64_t *)data_ptr;
  int type = (int)(first_word & 0xffU);
  switch (type) {
    case WOTA_INT: {
      long long val;
      data_ptr = wota_read_int (&val, data_ptr);
      *out_val = JS_NewInt64 (ctx, val);
      break;
    }
    case WOTA_FLOAT: {
      double d;
      data_ptr = wota_read_float (&d, data_ptr);
      *out_val = JS_NewFloat64 (ctx, d);
      break;
    }
    case WOTA_SYM: {
      int scode;
      data_ptr = wota_read_sym (&scode, data_ptr);
      if (scode == WOTA_PRIVATE) {
        JSGCRef inner_ref, obj_ref2;
        JS_PushGCRef (ctx, &inner_ref);
        JS_PushGCRef (ctx, &obj_ref2);
        inner_ref.val = JS_NULL;
        data_ptr = decode_wota_value (ctx, data_ptr, &inner_ref.val, holder, JS_NULL, reviver);
        obj_ref2.val = JS_NewObject (ctx);
        if (!JS_IsNull (ctx->actor_sym))
          JS_SetPropertyKey (ctx, obj_ref2.val, ctx->actor_sym, inner_ref.val);
        JS_CellStone (ctx, obj_ref2.val);
        *out_val = obj_ref2.val;
        JS_PopGCRef (ctx, &obj_ref2);
        JS_PopGCRef (ctx, &inner_ref);
      } else if (scode == WOTA_NULL) *out_val = JS_NULL;
      else if (scode == WOTA_FALSE) *out_val = JS_NewBool (ctx, 0);
      else if (scode == WOTA_TRUE) *out_val = JS_NewBool (ctx, 1);
      else *out_val = JS_NULL;
      break;
    }
    case WOTA_BLOB: {
      long long blen;
      char *bdata = NULL;
      data_ptr = wota_read_blob (&blen, &bdata, data_ptr);
      *out_val = bdata ? js_new_blob_stoned_copy (ctx, (uint8_t *)bdata, (size_t)blen) : js_new_blob_stoned_copy (ctx, NULL, 0);
      if (bdata) sys_free (bdata);
      break;
    }
    case WOTA_TEXT: {
      char *utf8 = NULL;
      data_ptr = wota_read_text (&utf8, data_ptr);
      *out_val = JS_NewString (ctx, utf8 ? utf8 : "");
      if (utf8) sys_free (utf8);
      break;
    }
    case WOTA_ARR: {
      long long c;
      data_ptr = wota_read_array (&c, data_ptr);
      JSGCRef arr_ref;
      JS_PushGCRef (ctx, &arr_ref);
      arr_ref.val = JS_NewArrayLen (ctx, c);
      for (long long i = 0; i < c; i++) {
        JSGCRef elem_ref;
        JS_PushGCRef (ctx, &elem_ref);
        elem_ref.val = JS_NULL;
        JSValue idx_key = JS_NewInt32 (ctx, (int32_t)i);
        data_ptr = decode_wota_value (ctx, data_ptr, &elem_ref.val, arr_ref.val, idx_key, reviver);
        JS_SetPropertyNumber (ctx, arr_ref.val, i, elem_ref.val);
        JS_PopGCRef (ctx, &elem_ref);
      }
      *out_val = arr_ref.val;
      JS_PopGCRef (ctx, &arr_ref);
      break;
    }
    case WOTA_REC: {
      long long c;
      data_ptr = wota_read_record (&c, data_ptr);
      JSGCRef obj_ref;
      JS_PushGCRef (ctx, &obj_ref);
      obj_ref.val = JS_NewObject (ctx);
      for (long long i = 0; i < c; i++) {
        char *tkey = NULL;
        size_t key_len;
        data_ptr = wota_read_text_len (&key_len, &tkey, data_ptr);
        if (!tkey) continue;
        JSGCRef prop_key_ref, sub_val_ref;
        JS_PushGCRef (ctx, &prop_key_ref);
        JS_PushGCRef (ctx, &sub_val_ref);
        prop_key_ref.val = JS_NewStringLen (ctx, tkey, key_len);
        sub_val_ref.val = JS_NULL;
        data_ptr = decode_wota_value (ctx, data_ptr, &sub_val_ref.val, obj_ref.val, prop_key_ref.val, reviver);
        JS_SetPropertyStr (ctx, obj_ref.val, tkey, sub_val_ref.val);
        JS_PopGCRef (ctx, &sub_val_ref);
        JS_PopGCRef (ctx, &prop_key_ref);
        sys_free (tkey);
      }
      *out_val = obj_ref.val;
      JS_PopGCRef (ctx, &obj_ref);
      break;
    }
    default:
      data_ptr += 8;
      *out_val = JS_NULL;
      break;
  }
  if (!JS_IsNull (reviver)) {
    JSValue key_val = JS_IsNull (key) ? JS_NULL : JS_DupValue (ctx, key);
    JSValue args[2] = { key_val, JS_DupValue (ctx, *out_val) };
    JSValue revived = JS_Call (ctx, reviver, holder, 2, args);
    JS_FreeValue (ctx, args[0]);
    JS_FreeValue (ctx, args[1]);
    if (!JS_IsException (revived)) {
      JS_FreeValue (ctx, *out_val);
      *out_val = revived;
    } else
      JS_FreeValue (ctx, revived);
  }
  return data_ptr;
}

void *value2wota (JSContext *ctx, JSValue v, JSValue replacer, size_t *bytes) {
  JSGCRef val_ref, rep_ref;
  JS_PushGCRef (ctx, &val_ref);
  JS_PushGCRef (ctx, &rep_ref);
  val_ref.val = v;
  rep_ref.val = replacer;

  WotaEncodeContext enc_s, *enc = &enc_s;

  enc->ctx = ctx;
  enc->visited_stack = NULL;
  enc->cycle = 0;
  enc->replacer = rep_ref.val;
  wota_buffer_init (&enc->wb, 16);
  wota_encode_value (enc, val_ref.val, JS_NULL, JS_NULL);
  if (enc->cycle) {
    wota_stack_free (enc);
    wota_buffer_free (&enc->wb);
    JS_PopGCRef (ctx, &rep_ref);
    JS_PopGCRef (ctx, &val_ref);
    return NULL;
  }
  wota_stack_free (enc);
  size_t total_bytes = enc->wb.size * sizeof (uint64_t);
  void *wota = sys_realloc (enc->wb.data, total_bytes);
  if (bytes) *bytes = total_bytes;
  JS_PopGCRef (ctx, &rep_ref);
  JS_PopGCRef (ctx, &val_ref);
  return wota;
}

JSValue wota2value (JSContext *ctx, void *wota) {
  JSGCRef holder_ref, result_ref;
  JS_PushGCRef (ctx, &holder_ref);
  JS_PushGCRef (ctx, &result_ref);
  result_ref.val = JS_NULL;
  holder_ref.val = JS_NewObject (ctx);
  decode_wota_value (ctx, wota, &result_ref.val, holder_ref.val, JS_NULL, JS_NULL);
  JSValue result = result_ref.val;
  JS_PopGCRef (ctx, &result_ref);
  JS_PopGCRef (ctx, &holder_ref);
  return result;
}

static JSValue js_wota_encode (JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
  if (argc < 1) return JS_RaiseDisrupt (ctx, "wota.encode requires at least 1 argument");
  size_t total_bytes;
  void *wota = value2wota (ctx, argv[0], JS_IsFunction (argv[1]) ? argv[1] : JS_NULL, &total_bytes);
  JSValue ret = js_new_blob_stoned_copy (ctx, wota, total_bytes);
  sys_free (wota);
  return ret;
}

static JSValue js_wota_decode (JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
  if (argc < 1) return JS_NULL;
  size_t len;
  uint8_t *buf = js_get_blob_data (ctx, &len, argv[0]);
  if (buf == (uint8_t *)-1) return JS_EXCEPTION;
  if (!buf || len == 0) return JS_RaiseDisrupt (ctx, "No blob data present");
  JSValue reviver = (argc > 1 && JS_IsFunction (argv[1])) ? argv[1] : JS_NULL;
  char *data_ptr = (char *)buf;
  JSGCRef result_ref, holder_ref, empty_key_ref;
  JS_PushGCRef (ctx, &result_ref);
  JS_PushGCRef (ctx, &holder_ref);
  JS_PushGCRef (ctx, &empty_key_ref);
  result_ref.val = JS_NULL;
  holder_ref.val = JS_NewObject (ctx);
  empty_key_ref.val = JS_NewString (ctx, "");
  decode_wota_value (ctx, data_ptr, &result_ref.val, holder_ref.val, empty_key_ref.val, reviver);
  JSValue result = result_ref.val;
  JS_PopGCRef (ctx, &empty_key_ref);
  JS_PopGCRef (ctx, &holder_ref);
  JS_PopGCRef (ctx, &result_ref);
  return result;
}

static const JSCFunctionListEntry js_wota_funcs[] = {
  JS_CFUNC_DEF ("encode", 2, js_wota_encode),
  JS_CFUNC_DEF ("decode", 2, js_wota_decode),
};

JSValue js_core_internal_wota_use (JSContext *ctx) {
  JSGCRef exports_ref;
  JS_PushGCRef (ctx, &exports_ref);
  exports_ref.val = JS_NewObject (ctx);
  JS_SetPropertyFunctionList (ctx, exports_ref.val, js_wota_funcs, sizeof (js_wota_funcs) / sizeof (js_wota_funcs[0]));
  JSValue result = exports_ref.val;
  JS_PopGCRef (ctx, &exports_ref);
  return result;
}
