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

static int nota_get_arr_len (JSContext *ctx, JSValue arr) {
  int64_t len;
  JS_GetLength (ctx, arr, &len);
  return (int)len;
}

typedef struct NotaVisitedNode {
  JSGCRef ref;
  struct NotaVisitedNode *next;
} NotaVisitedNode;

typedef struct NotaEncodeContext {
  JSContext *ctx;
  NotaVisitedNode *visited_list;
  NotaBuffer nb;
  int cycle;
  JSGCRef *replacer_ref; /* pointer to GC-rooted ref */
} NotaEncodeContext;

static void nota_stack_push (NotaEncodeContext *enc, JSValueConst val) {
  NotaVisitedNode *node = (NotaVisitedNode *)sys_malloc (sizeof (NotaVisitedNode));
  JS_PushGCRef (enc->ctx, &node->ref);
  node->ref.val = JS_DupValue (enc->ctx, val);
  node->next = enc->visited_list;
  enc->visited_list = node;
}

static void nota_stack_pop (NotaEncodeContext *enc) {
  NotaVisitedNode *node = enc->visited_list;
  enc->visited_list = node->next;
  JS_FreeValue (enc->ctx, node->ref.val);
  JS_PopGCRef (enc->ctx, &node->ref);
  sys_free (node);
}

static int nota_stack_has (NotaEncodeContext *enc, JSValueConst val) {
  NotaVisitedNode *node = enc->visited_list;
  while (node) {
    if (JS_StrictEq (enc->ctx, node->ref.val, val))
      return 1;
    node = node->next;
  }
  return 0;
}

static JSValue nota_apply_replacer (NotaEncodeContext *enc, JSValueConst holder, JSValueConst key, JSValueConst val) {
  if (!enc->replacer_ref || JS_IsNull (enc->replacer_ref->val)) return JS_DupValue (enc->ctx, val);

  JSValue args[2] = { JS_DupValue (enc->ctx, key), JS_DupValue (enc->ctx, val) };
  JSValue result = JS_Call (enc->ctx, enc->replacer_ref->val, 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 char *js_do_nota_decode (JSContext *js, JSValue *tmp, char *nota, JSValue holder, JSValue key, JSValue reviver) {
  int type = nota_type (nota);
  JSValue ret2;
  long long n;
  double d;
  int b;
  char *str;
  uint8_t *blob;

  switch (type) {
    case NOTA_BLOB:
      nota = nota_read_blob (&n, (char **)&blob, nota);
      *tmp = js_new_blob_stoned_copy (js, blob, n);
      sys_free (blob);
      break;
    case NOTA_TEXT:
      nota = nota_read_text (&str, nota);
      *tmp = JS_NewString (js, str);
      sys_free (str);
      break;
    case NOTA_ARR:
      nota = nota_read_array (&n, nota);
      *tmp = JS_NewArrayLen (js, n);
      for (int i = 0; i < n; i++) {
        nota = js_do_nota_decode (js, &ret2, nota, *tmp, JS_NewInt32 (js, i), reviver);
        JS_SetPropertyNumber (js, *tmp, i, ret2);
      }
      break;
    case NOTA_REC:
      nota = nota_read_record (&n, nota);
      *tmp = JS_NewObject (js);
      for (int i = 0; i < n; i++) {
        JSGCRef prop_key_ref, sub_val_ref;
        JS_PushGCRef (js, &prop_key_ref);
        JS_PushGCRef (js, &sub_val_ref);
        nota = nota_read_text (&str, nota);
        prop_key_ref.val = JS_NewString (js, str);
        sub_val_ref.val = JS_NULL;
        nota = js_do_nota_decode (js, &sub_val_ref.val, nota, *tmp, prop_key_ref.val, reviver);
        JS_SetPropertyStr (js, *tmp, str, sub_val_ref.val);
        JS_PopGCRef (js, &sub_val_ref);
        JS_PopGCRef (js, &prop_key_ref);
        sys_free (str);
      }
      break;
    case NOTA_INT:
      nota = nota_read_int (&n, nota);
      *tmp = JS_NewInt64 (js, n);
      break;
    case NOTA_SYM:
      nota = nota_read_sym (&b, nota);
      if (b == NOTA_PRIVATE) {
        JSGCRef inner_ref, obj_ref2;
        JS_PushGCRef (js, &inner_ref);
        JS_PushGCRef (js, &obj_ref2);
        inner_ref.val = JS_NULL;
        nota = js_do_nota_decode (js, &inner_ref.val, nota, holder, JS_NULL, reviver);
        obj_ref2.val = JS_NewObject (js);
        if (!JS_IsNull (js->actor_sym))
          JS_SetPropertyKey (js, obj_ref2.val, js->actor_sym, inner_ref.val);
        JS_CellStone (js, obj_ref2.val);
        *tmp = obj_ref2.val;
        JS_PopGCRef (js, &obj_ref2);
        JS_PopGCRef (js, &inner_ref);
      } else {
        switch (b) {
          case NOTA_NULL: *tmp = JS_NULL; break;
          case NOTA_FALSE: *tmp = JS_NewBool (js, 0); break;
          case NOTA_TRUE: *tmp = JS_NewBool (js, 1); break;
          default: *tmp = JS_NULL; break;
        }
      }
      break;
    default:
    case NOTA_FLOAT:
      nota = nota_read_float (&d, nota);
      *tmp = JS_NewFloat64 (js, d);
      break;
  }

  if (!JS_IsNull (reviver)) {
    JSValue args[2] = { JS_DupValue (js, key), JS_DupValue (js, *tmp) };
    JSValue revived = JS_Call (js, reviver, holder, 2, args);
    JS_FreeValue (js, args[0]);
    JS_FreeValue (js, args[1]);
    if (!JS_IsException (revived)) {
      JS_FreeValue (js, *tmp);
      *tmp = revived;
    } else {
      JS_FreeValue (js, revived);
    }
  }

  return nota;
}

static void nota_encode_value (NotaEncodeContext *enc, JSValueConst val, JSValueConst holder, JSValueConst key) {
  JSContext *ctx = enc->ctx;
  JSGCRef replaced_ref, keys_ref, elem_ref, prop_ref;
  JS_PushGCRef (ctx, &replaced_ref);
  replaced_ref.val = nota_apply_replacer (enc, holder, key, val);
  int tag = JS_VALUE_GET_TAG (replaced_ref.val);

  switch (tag) {
    case JS_TAG_INT:
    case JS_TAG_FLOAT64: {
      double d;
      JS_ToFloat64 (ctx, &d, replaced_ref.val);
      nota_write_number (&enc->nb, d);
      break;
    }
    case JS_TAG_STRING: {
      const char *str = JS_ToCString (ctx, replaced_ref.val);
      nota_write_text (&enc->nb, str);
      JS_FreeCString (ctx, str);
      break;
    }
    case JS_TAG_BOOL:
      if (JS_VALUE_GET_BOOL (replaced_ref.val)) nota_write_sym (&enc->nb, NOTA_TRUE);
      else nota_write_sym (&enc->nb, NOTA_FALSE);
      break;
    case JS_TAG_NULL:
      nota_write_sym (&enc->nb, NOTA_NULL);
      break;
    case JS_TAG_PTR: {
      if (JS_IsText (replaced_ref.val)) {
        const char *str = JS_ToCString (ctx, replaced_ref.val);
        nota_write_text (&enc->nb, str);
        JS_FreeCString (ctx, str);
        break;
      }

      if (js_is_blob (ctx, replaced_ref.val)) {
        size_t buf_len;
        void *buf_data = js_get_blob_data (ctx, &buf_len, replaced_ref.val);
        if (buf_data == (void *)-1) {
          JS_PopGCRef (ctx, &replaced_ref);
          return;
        }
        nota_write_blob (&enc->nb, (unsigned long long)buf_len * 8, (const char *)buf_data);
        break;
      }

      if (JS_IsArray (replaced_ref.val)) {
        if (nota_stack_has (enc, replaced_ref.val)) {
          enc->cycle = 1;
          break;
        }
        nota_stack_push (enc, replaced_ref.val);
        int arr_len = nota_get_arr_len (ctx, replaced_ref.val);
        nota_write_array (&enc->nb, arr_len);
        JS_PushGCRef (ctx, &elem_ref);
        for (int i = 0; i < arr_len; i++) {
          elem_ref.val = JS_GetPropertyNumber (ctx, replaced_ref.val, i);
          JSValue elem_key = JS_NewInt32 (ctx, i);
          nota_encode_value (enc, elem_ref.val, replaced_ref.val, elem_key);
        }
        JS_PopGCRef (ctx, &elem_ref);
        nota_stack_pop (enc);
        break;
      }

      JSValue adata = JS_NULL;
      if (!JS_IsNull (ctx->actor_sym)) {
        int has = JS_HasPropertyKey (ctx, replaced_ref.val, ctx->actor_sym);
        if (has > 0) adata = JS_GetPropertyKey (ctx, replaced_ref.val, ctx->actor_sym);
      }
      if (!JS_IsNull (adata)) {
        nota_write_sym (&enc->nb, NOTA_PRIVATE);
        nota_encode_value (enc, adata, replaced_ref.val, JS_NULL);
        JS_FreeValue (ctx, adata);
        break;
      }
      JS_FreeValue (ctx, adata);
      if (nota_stack_has (enc, replaced_ref.val)) {
        enc->cycle = 1;
        break;
      }
      nota_stack_push (enc, replaced_ref.val);

      JSValue to_json = JS_GetPropertyStr (ctx, replaced_ref.val, "toJSON");
      if (JS_IsFunction (to_json)) {
        JSValue result = JS_Call (ctx, to_json, replaced_ref.val, 0, NULL);
        if (!JS_IsException (result)) {
          nota_encode_value (enc, result, holder, key);
        } else {
          nota_write_sym (&enc->nb, NOTA_NULL);
        }
        nota_stack_pop (enc);
        break;
      }

      JS_PushGCRef (ctx, &keys_ref);
      keys_ref.val = JS_GetOwnPropertyNames (ctx, replaced_ref.val);
      if (JS_IsException (keys_ref.val)) {
        nota_write_sym (&enc->nb, NOTA_NULL);
        nota_stack_pop (enc);
        JS_PopGCRef (ctx, &keys_ref);
        break;
      }
      int64_t plen64;
      if (JS_GetLength (ctx, keys_ref.val, &plen64) < 0) {
        nota_write_sym (&enc->nb, NOTA_NULL);
        nota_stack_pop (enc);
        JS_PopGCRef (ctx, &keys_ref);
        break;
      }
      uint32_t plen = (uint32_t)plen64;

      JS_PushGCRef (ctx, &prop_ref);
      JS_PushGCRef (ctx, &elem_ref);
      uint32_t non_function_count = 0;
      for (uint32_t i = 0; i < plen; i++) {
        elem_ref.val = JS_GetPropertyNumber (ctx, keys_ref.val, i);
        prop_ref.val = JS_GetProperty (ctx, replaced_ref.val, elem_ref.val);
        if (!JS_IsFunction (prop_ref.val)) non_function_count++;
      }

      nota_write_record (&enc->nb, non_function_count);
      for (uint32_t i = 0; i < plen; i++) {
        elem_ref.val = JS_GetPropertyNumber (ctx, keys_ref.val, i);
        prop_ref.val = JS_GetProperty (ctx, replaced_ref.val, elem_ref.val);
        if (!JS_IsFunction (prop_ref.val)) {
          const char *prop_name = JS_ToCString (ctx, elem_ref.val);
          nota_write_text (&enc->nb, prop_name ? prop_name : "");
          nota_encode_value (enc, prop_ref.val, replaced_ref.val, elem_ref.val);
          JS_FreeCString (ctx, prop_name);
        }
      }
      JS_PopGCRef (ctx, &elem_ref);
      JS_PopGCRef (ctx, &prop_ref);
      JS_PopGCRef (ctx, &keys_ref);
      nota_stack_pop (enc);
      break;
    }
    default:
      nota_write_sym (&enc->nb, NOTA_NULL);
      break;
  }
  JS_PopGCRef (ctx, &replaced_ref);
}

void *value2nota (JSContext *ctx, JSValue v) {
  JSGCRef val_ref, key_ref;
  JS_PushGCRef (ctx, &val_ref);
  JS_PushGCRef (ctx, &key_ref);
  val_ref.val = v;

  NotaEncodeContext enc_s, *enc = &enc_s;
  enc->ctx = ctx;
  enc->visited_list = NULL;
  enc->cycle = 0;
  enc->replacer_ref = NULL;

  nota_buffer_init (&enc->nb, 128);
  key_ref.val = JS_NewString (ctx, "");
  nota_encode_value (enc, val_ref.val, JS_NULL, key_ref.val);

  if (enc->cycle) {
    JS_PopGCRef (ctx, &key_ref);
    JS_PopGCRef (ctx, &val_ref);
    nota_buffer_free (&enc->nb);
    return NULL;
  }

  JS_PopGCRef (ctx, &key_ref);
  JS_PopGCRef (ctx, &val_ref);
  void *data_ptr = enc->nb.data;
  enc->nb.data = NULL;
  nota_buffer_free (&enc->nb);
  return data_ptr;
}

JSValue nota2value (JSContext *js, void *nota) {
  if (!nota) return JS_NULL;
  JSGCRef holder_ref, key_ref, ret_ref;
  JS_PushGCRef (js, &holder_ref);
  JS_PushGCRef (js, &key_ref);
  JS_PushGCRef (js, &ret_ref);
  holder_ref.val = JS_NewObject (js);
  key_ref.val = JS_NewString (js, "");
  ret_ref.val = JS_NULL;
  js_do_nota_decode (js, &ret_ref.val, nota, holder_ref.val, key_ref.val, JS_NULL);
  JSValue result = ret_ref.val;
  JS_PopGCRef (js, &ret_ref);
  JS_PopGCRef (js, &key_ref);
  JS_PopGCRef (js, &holder_ref);
  return result;
}

static JSValue js_nota_encode (JSContext *ctx, JSValueConst this_val, int argc, JSValueConst *argv) {
  if (argc < 1) return JS_RaiseDisrupt (ctx, "nota.encode requires at least 1 argument");

  JSGCRef val_ref, replacer_ref, key_ref;
  JS_PushGCRef (ctx, &val_ref);
  JS_PushGCRef (ctx, &replacer_ref);
  JS_PushGCRef (ctx, &key_ref);
  val_ref.val = argv[0];
  replacer_ref.val = (argc > 1 && JS_IsFunction (argv[1])) ? argv[1] : JS_NULL;

  NotaEncodeContext enc_s, *enc = &enc_s;
  enc->ctx = ctx;
  enc->visited_list = NULL;
  enc->cycle = 0;
  enc->replacer_ref = &replacer_ref;

  nota_buffer_init (&enc->nb, 128);
  key_ref.val = JS_NewString (ctx, "");
  nota_encode_value (enc, val_ref.val, JS_NULL, key_ref.val);

  JSValue ret;
  if (enc->cycle) {
    nota_buffer_free (&enc->nb);
    ret = JS_RaiseDisrupt (ctx, "Tried to encode something to nota with a cycle.");
  } else {
    size_t total_len = enc->nb.size;
    void *data_ptr = enc->nb.data;
    ret = js_new_blob_stoned_copy (ctx, (uint8_t *)data_ptr, total_len);
    nota_buffer_free (&enc->nb);
  }

  JS_PopGCRef (ctx, &key_ref);
  JS_PopGCRef (ctx, &replacer_ref);
  JS_PopGCRef (ctx, &val_ref);
  return ret;
}

static JSValue js_nota_decode (JSContext *js, JSValueConst self, int argc, JSValueConst *argv) {
  if (argc < 1) return JS_NULL;

  size_t len;
  unsigned char *nota = js_get_blob_data (js, &len, argv[0]);
  if (nota == (unsigned char *)-1) return JS_EXCEPTION;
  if (!nota) return JS_NULL;

  JSGCRef holder_ref, key_ref, ret_ref, reviver_ref;
  JS_PushGCRef (js, &holder_ref);
  JS_PushGCRef (js, &key_ref);
  JS_PushGCRef (js, &ret_ref);
  JS_PushGCRef (js, &reviver_ref);

  reviver_ref.val = (argc > 1 && JS_IsFunction (argv[1])) ? argv[1] : JS_NULL;
  holder_ref.val = JS_NewObject (js);
  key_ref.val = JS_NewString (js, "");
  ret_ref.val = JS_NULL;

  js_do_nota_decode (js, &ret_ref.val, (char *)nota, holder_ref.val, key_ref.val, reviver_ref.val);

  JSValue result = ret_ref.val;
  JS_PopGCRef (js, &reviver_ref);
  JS_PopGCRef (js, &ret_ref);
  JS_PopGCRef (js, &key_ref);
  JS_PopGCRef (js, &holder_ref);
  return result;
}

static const JSCFunctionListEntry js_nota_funcs[] = {
  JS_CFUNC_DEF ("encode", 1, js_nota_encode),
  JS_CFUNC_DEF ("decode", 1, js_nota_decode),
};

JSValue js_core_internal_nota_use (JSContext *js) {
  JSGCRef export_ref;
  JS_PushGCRef (js, &export_ref);
  export_ref.val = JS_NewObject (js);
  JS_SetPropertyFunctionList (js, export_ref.val, js_nota_funcs, sizeof (js_nota_funcs) / sizeof (JSCFunctionListEntry));
  JSValue result = export_ref.val;
  JS_PopGCRef (js, &export_ref);
  return result;
}
