#include "postgres.h"

#include "catalog/pg_type_d.h"
#include "executor/spi.h"
#include "fmgr.h"
#include "funcapi.h"
#include "parser/parse_coerce.h"
#include "utils/array.h"
#include "utils/builtins.h"
#include "utils/date.h"
#include "utils/jsonb.h"
#include "utils/lsyscache.h"
#include "utils/palloc.h"
#include "utils/timestamp.h"
#include "utils/typcache.h"

#include "deps/quickjs/quickjs.h"

#include "pljs.h"

#include <string.h>

/*
 * Error handling helper macros for consistent error patterns.
 */
#define PLJS_THROW_IF_NULL(ptr, msg, ctx)                                      \
  do {                                                                         \
    if ((ptr) == NULL) {                                                       \
      return js_throw((msg), (ctx));                                           \
    }                                                                          \
  } while (0)

#define PLJS_THROW_TYPE_ERROR(expected, ctx)                                   \
  js_throw("expected " expected " type", (ctx))

// Helper functions that should really exist as part of quickjs.
static JSClassID JS_CLASS_OBJECT = 1;
static JSClassID JS_CLASS_DATE = 10;
static JSClassID JS_CLASS_ARRAY_BUFFER = 19;
static JSClassID JS_CLASS_SHARED_ARRAY_BUFFER = 20;
static JSClassID JS_CLASS_UINT8C_ARRAY = 21;
static JSClassID JS_CLASS_INT8_ARRAY = 22;
static JSClassID JS_CLASS_UINT8_ARRAY = 23;
static JSClassID JS_CLASS_INT16_ARRAY = 24;
static JSClassID JS_CLASS_UINT16_ARRAY = 25;
static JSClassID JS_CLASS_INT32_ARRAY = 26;
static JSClassID JS_CLASS_UINT32_ARRAY = 27;

/**
 * Struct containing the type information for a catch-all*/
// if given object is an array.
inline static bool Is_ArrayType(JSValueConst obj, JSClassID class_id) {
  return NULL != JS_GetOpaque(obj, class_id);
}

// if given object is array buffer.
inline static bool Is_ArrayBuffer(JSValueConst obj) {
  return NULL != JS_GetOpaque(obj, JS_CLASS_ARRAY_BUFFER);
}

// if given object is shared array buffer.
inline static bool Is_SharedArrayBuffer(JSValueConst obj) {
  return NULL != JS_GetOpaque(obj, JS_CLASS_SHARED_ARRAY_BUFFER);
}

// if this is an actual object of any sort.
inline static bool Is_Object(JSValueConst obj) {
  return NULL != JS_GetOpaque(obj, JS_CLASS_OBJECT);
}

// if given object is shared array buffer.
inline static bool Is_Date(JSValueConst obj) {
  return NULL != JS_GetOpaque(obj, JS_CLASS_DATE);
}

#if JSONB_DIRECT_CONVERSION
static JSValue convert_jsonb(JsonbContainer *in, JSContext *ctx);
static JSValue get_jsonb_value(JsonbValue *scalarVal, JSContext *ctx);
static Jsonb *convert_object(JSValue object, JSContext *ctx);
#endif

/**
 * @brief Converts a Javascript epoch to a Datum.
 *
 * @param @c double Javascript epoch
 * @returns #Datum of type `DATEADT`
 */
static Datum pljs_convert_epoch_to_date(double epoch) {
  epoch -= (POSTGRES_EPOCH_JDATE - UNIX_EPOCH_JDATE) * 86400000.0;

#ifdef HAVE_INT64_TIMESTAMP
  epoch = (epoch * 1000) / USECS_PER_DAY;
#else
  epoch = (epoch / 1000) / SECS_PER_DAY;
#endif
  PG_RETURN_DATEADT((DateADT)epoch);
}

/**
 * @brief Converts a Javascript epoch to a Datum.
 *
 * @param @c double Javascript epoch
 * @returns #Datum of a timestamptz
 */
static Datum pljs_convert_epoch_to_timestamptz(double epoch) {
  epoch -= (POSTGRES_EPOCH_JDATE - UNIX_EPOCH_JDATE) * 86400000.0;

#ifdef HAVE_INT64_TIMESTAMP
  return Int64GetDatum((int64)epoch * 1000);
#else
  return Float8GetDatum(epoch / 1000.0);
#endif
}

/**
 * @brief Converts a `DateADT` Datum to a Javascript epoch.
 *
 * @param #Datum of type `DateADT`
 * @returns @c double Javascript epoch
 */
static double pljs_convert_date_to_epoch(DateADT date) {
  double epoch;

#ifdef HAVE_INT64_TIMESTAMP
  epoch = (double)date * USECS_PER_DAY / 1000.0;
#else
  epoch = (double)date * SECS_PER_DAY * 1000.0;
#endif

  return epoch + (POSTGRES_EPOCH_JDATE - UNIX_EPOCH_JDATE) * 86400000.0;
}

/**
 * @brief Converts a `TimestampTz` Datum to a Javascript epoch.
 *
 * @param #Datum of type `TimestampTz`
 * @returns @c double Javascript epoch
 */
static double pljs_convert_timestamptz_to_epoch(TimestampTz tm) {
  double epoch;

#ifdef HAVE_INT64_TIMESTAMP
  epoch = (double)tm / 1000.0;
#else
  epoch = (double)tm * 1000.0;
#endif

  return epoch + (POSTGRES_EPOCH_JDATE - UNIX_EPOCH_JDATE) * 86400000.0;
}

/**
 * @brief Makes a copy of a #text type from Postgres and returns a `cstring`.
 *
 * Takes the input of a Postgres `TEXT` field, allocates memory in the
 * current memory context, and returns a `\0` terminated copy of the string
 * that was stored.  It is up to the caller to free the memory allocated.
 *
 * @param what #text - string to duplicate
 * @returns @c char * copy of the text field
 */
static char *pljs_util_dup_pgtext(text *what) {
  size_t len = VARSIZE(what) - VARHDRSZ;
  char *dup = palloc(len + 1);

  memcpy(dup, VARDATA(what), len);
  dup[len] = 0;

  return dup;
}

/**
 * @brief Converts an SPI status into static text.
 */
static const char *pljs_util_spi_status_string(int status) {
  static char private_buf[1024];

  if (status > 0)
    return "OK";

  switch (status) {
  case SPI_ERROR_CONNECT:
    return "SPI_ERROR_CONNECT";
  case SPI_ERROR_COPY:
    return "SPI_ERROR_COPY";
  case SPI_ERROR_OPUNKNOWN:
    return "SPI_ERROR_OPUNKNOWN";
  case SPI_ERROR_UNCONNECTED:
  case SPI_ERROR_TRANSACTION:
    return "current transaction is aborted, "
           "commands ignored until end of transaction block";
  case SPI_ERROR_CURSOR:
    return "SPI_ERROR_CURSOR";
  case SPI_ERROR_ARGUMENT:
    return "SPI_ERROR_ARGUMENT";
  case SPI_ERROR_PARAM:
    return "SPI_ERROR_PARAM";
  case SPI_ERROR_NOATTRIBUTE:
    return "SPI_ERROR_NOATTRIBUTE";
  case SPI_ERROR_NOOUTFUNC:
    return "SPI_ERROR_NOOUTFUNC";
  case SPI_ERROR_TYPUNKNOWN:
    return "SPI_ERROR_TYPUNKNOWN";
  default:
    snprintf(private_buf, sizeof(private_buf), "SPI_ERROR: %d", status);
    return private_buf;
  }
}

/**
 * @brief Helper for getting the length of a Javascript array.
 *
 * @param obj JSValueConst - Javascript array to check the length of
 * @param ctx #JSContext - Javascript context to execute in
 * @returns @c uint32_t
 */
uint32_t pljs_js_array_length(JSValueConst obj, JSContext *ctx) {
  JSValue length = JS_GetPropertyStr(ctx, obj, "length");
  int32_t array_length_int;
  JS_ToInt32(ctx, &array_length_int, length);

  return array_length_int;
}

/**
 * @brief Converts an `Oid` into `pljs_type`.
 *
 * Takes an input of a pointer to `pljs_type` and an `Oid`,
 * and queries Postgres for enough information for type conversions
 * between Postgres and Javascript.
 *
 * @param type #pljs_type - the location to store the type data
 * @param typid #Oid - the Postgres type to decode
 */
void pljs_type_fill(pljs_type *type, Oid typid) {
  bool is_preferred;
  type->typid = typid;

  get_type_category_preferred(typid, &type->category, &is_preferred);

  type->is_composite = (type->category == TYPCATEGORY_COMPOSITE);

  get_typlenbyvalalign(typid, &type->length, &type->byval, &type->align);

  if (type->category == TYPCATEGORY_ARRAY) {
    Oid elemid = get_element_type(typid);

    if (elemid == InvalidOid) {
      ereport(ERROR,
              (errmsg("cannot determine element type of array: %u", typid)));
    }

    type->typid = elemid;
    type->is_composite = (TypeCategory(elemid) == TYPCATEGORY_COMPOSITE);
    get_typlenbyvalalign(type->typid, &type->length, &type->byval,
                         &type->align);
  } else if (type->category == TYPCATEGORY_PSEUDOTYPE) {
    type->is_composite = true;
  }
}

/**
 * @brief Helper to get or lookup a TupleDesc with consistent ownership
 * semantics.
 *
 * If a TupleDesc is provided, it is used directly and needs_release is set to
 * false. If NULL is provided, a TupleDesc is looked up from the type OID and
 * needs_release is set to true, indicating the caller must call
 * ReleaseTupleDesc when done.
 *
 * @param typid #Oid - the type OID to look up if provided is NULL
 * @param provided #TupleDesc - optional pre-existing TupleDesc to use
 * @param needs_release @c bool* - output indicating if caller must release
 * @returns #TupleDesc the tuple descriptor to use
 */
static TupleDesc pljs_get_tupdesc(Oid typid, TupleDesc provided,
                                  bool *needs_release) {
  if (provided != NULL) {
    *needs_release = false;
    return provided;
  }

  *needs_release = true;
  return lookup_rowtype_tupdesc(typid, -1);
}

/**
 * @brief Converts a #Datum for a Javascript object.
 *
 * Takes a #Datum and converts it to a Javascript object.  If there is
 * an error, throws a Javascript exception.
 *
 * @param type #pljs_type - type information of the #Datum
 * @param arg #Datum - value to convert
 * @param ctx #JSContext - Javascript context to execute in
 * @returns #JSValue of the object or thrown exception in case of error
 */
JSValue pljs_datum_to_object(pljs_type *type, Datum arg, JSContext *ctx) {
  if (arg == 0) {
    return JS_UNDEFINED;
  }

  JSValue obj;

  HeapTupleHeader rec = DatumGetHeapTupleHeader(arg);
  Oid tupType;
  int32 tupTypmod;
  TupleDesc tupdesc = NULL;
  HeapTupleData tuple;

  PG_TRY();
  {
    /* Extract type info from the tuple itself. */
    tupType = HeapTupleHeaderGetTypeId(rec);
    tupTypmod = HeapTupleHeaderGetTypMod(rec);
    tupdesc = lookup_rowtype_tupdesc(tupType, tupTypmod);
  }
  PG_CATCH();
  {
    ErrorData *edata = CopyErrorData();
    JSValue error = js_throw(edata->message, ctx);
    FlushErrorState();
    FreeErrorData(edata);

    return error;
  }
  PG_END_TRY();

  obj = JS_NewObject(ctx);

  if (tupdesc) {
    for (int16 i = 0; i < tupdesc->natts; i++) {
      Datum datum;
      bool isnull = false;

      if (TupleDescAttr(tupdesc, i)->attisdropped) {
        continue;
      }

      char *colname = NameStr(TupleDescAttr(tupdesc, i)->attname);
      tuple.t_len = HeapTupleHeaderGetDatumLength(rec);
      ItemPointerSetInvalid(&(tuple.t_self));
      tuple.t_tableOid = InvalidOid;
      tuple.t_data = rec;

      datum = heap_getattr(&tuple, i + 1, tupdesc, &isnull);

      JS_SetPropertyStr(
          ctx, obj, colname,
          pljs_datum_to_jsvalue(TupleDescAttr(tupdesc, i)->atttypid, datum,
                                isnull, true, ctx));
    }

    ReleaseTupleDesc(tupdesc);
  }

  return obj;
}

/**
 * @brief Converts a Postgres array to a Javascript array.
 *
 * Takes a Postgres #Datum and type and converts it into a Javascript
 * array.  All properties are set, including array length.
 *
 * @param type #pljs_type - type information for the array
 * @param arg #Datum - Postgres array to convert
 * @param ctx #JSContext - Javascript context to execute in
 * @returns #JSValue of the array
 */
JSValue pljs_datum_to_array(pljs_type *type, Datum arg, JSContext *ctx) {
  JSValue array = JS_NewArray(ctx);
  Datum *values;
  bool *nulls;
  int nelems;

  deconstruct_array(DatumGetArrayTypeP(arg), type->typid, type->length,
                    type->byval, type->align, &values, &nulls, &nelems);

  for (int i = 0; i < nelems; i++) {
    JSValue value =
        pljs_datum_to_jsvalue(type->typid, values[i], nulls[i], true, ctx);

    JS_SetPropertyUint32(ctx, array, i, value);
  }

  JSValue length = JS_NewInt32(ctx, nelems);
  JS_SetPropertyStr(ctx, array, "length", length);

  pfree(values);
  pfree(nulls);

  return array;
}

/**
 * @brief Fallback type conversion from @Datum to @JSValue.
 *
 * If a type is unknown, instead of returning `undefined` or `NULL`, do a
 * `cstring` or `varlena`, or value based conversion to an `String` or `Int32`
 * and back.
 *
 * In the case of fixed-length `INTERNALLENGTH`, it will be a `String`
 * with those bytes copied into the string, and the length set appropriately.
 * When the `String` is variable, the length would be taken from the `varlena`
 * header, and the rest copied into the string.
 *
 * @param arg #Datum - Postgres datum to convert
 * @param type #pljs_type - type of the datum
 * @param ctx #JSContext - Javascript context
 * @returns #JSValue conversion of the Datum
 */
static JSValue pljs_datum_to_jsvalue_fallback(Datum arg, pljs_type type,
                                              JSContext *ctx) {
  JSValue ret = JS_UNDEFINED;

  if (type.byval) {
    ret = JS_NewInt32(ctx, arg);
  } else {
    // If this is a variable length type, make a copy of it.
    if (type.length == -1) {
      ret = JS_NewStringLen(ctx, (char *)VARDATA(arg), VARSIZE_ANY_EXHDR(arg));
      JS_SetPropertyStr(ctx, ret, "length",
                        JS_NewInt32(ctx, VARSIZE_ANY_EXHDR(arg)));
    } else {
      ret = JS_NewStringLen(ctx, (char *)arg, type.length);
      JS_SetPropertyStr(ctx, ret, "length", JS_NewInt32(ctx, type.length));
    }
  }

  return ret;
}

/**
 * @brief Converts a Postgres #Datum to a Javascript value.
 *
 * Takes a Postgres #Datum and type and converts it into a Javascript
 * value.  If the type is an array or is composite, then call out to
 * the correct functions.
 *
 * @param argtype #Oid - type information for the type
 * @param arg #Datum - Postgres value to convert
 * @param is_null @c bool - whether the datum is null
 * @param expand_composite @c bool - whether to expand composite types to
 * objects
 * @param ctx #JSContext - Javascript context to execute in
 * @returns #JSValue of the value, or JS_NULL if null
 */
JSValue pljs_datum_to_jsvalue(Oid argtype, Datum arg, bool is_null,
                              bool expand_composite, JSContext *ctx) {
  // Handle null case explicitly
  if (is_null) {
    return JS_NULL;
  }

  JSValue return_result;
  char *str;

  pljs_type type;
  pljs_type_fill(&type, argtype);

  if (type.category == TYPCATEGORY_ARRAY) {
    return pljs_datum_to_array(&type, arg, ctx);
  }

  if (expand_composite && type.is_composite) {
    return pljs_datum_to_object(&type, arg, ctx);
  }

  switch (type.typid) {
  case OIDOID:
    return_result = JS_NewInt64(ctx, arg);
    break;

  case BOOLOID:
    return_result = JS_NewBool(ctx, DatumGetBool(arg));
    break;

  case INT2OID:
    return_result = JS_NewInt32(ctx, DatumGetInt16(arg));
    break;

  case INT4OID:
    return_result = JS_NewInt32(ctx, DatumGetInt32(arg));
    break;

  case INT8OID:
    return_result = JS_NewBigInt64(ctx, DatumGetInt64(arg));
    break;

  case FLOAT4OID:
    return_result = JS_NewFloat64(ctx, DatumGetFloat4(arg));
    break;

  case FLOAT8OID:
    return_result = JS_NewFloat64(ctx, DatumGetFloat8(arg));
    break;

  case NUMERICOID:
    return_result = JS_NewFloat64(
        ctx, DatumGetFloat8(DirectFunctionCall1(numeric_float8, arg)));
    break;

  case TEXTOID:
  case VARCHAROID:
  case BPCHAROID:
  case XMLOID:
    // Get a copy of the string.
    str = pljs_util_dup_pgtext(DatumGetTextP(arg));

    return_result = JS_NewString(ctx, str);

    // Free the memory allocated.
    pfree(str);
    break;

  case NAMEOID:
    return_result = JS_NewString(ctx, DatumGetName(arg)->data);
    break;

  case JSONOID:
    // Get a copy of the string.
    str = pljs_util_dup_pgtext(DatumGetTextP(arg));

    return_result = JS_ParseJSON(ctx, str, strlen(str), NULL);

    // free the memory allocated.
    pfree(str);
    break;

  case JSONBOID: {
#if JSONB_DIRECT_CONVERSION
    Jsonb *jsonb = (Jsonb *)PG_DETOAST_DATUM(arg);

    if (JB_ROOT_IS_SCALAR(jsonb)) {
      JsonbValue jb;
      JsonbExtractScalar(&jsonb->root, &jb);
      return_result = get_jsonb_value(&jb, ctx);
    } else {
      return_result = convert_jsonb(&jsonb->root, ctx);
    }
#else
    // Get the datum.
    Jsonb *jb = DatumGetJsonbP(arg);

    // Convert it to a string (takes some casting, but JsonbContainer is also
    // a varlena).
    str = JsonbToCString(NULL, (JsonbContainer *)VARDATA(jb), VARSIZE(jb));

    return_result = JS_ParseJSON(ctx, str, strlen(str), NULL);

    // Free the memory allocated.
    pfree(str);
#endif
    break;
  }

  case BYTEAOID: {
    void *p = PG_DETOAST_DATUM_COPY(arg);
    char *buf = palloc(VARSIZE_ANY_EXHDR(p) + 1);

    memcpy(buf, VARDATA(p), VARSIZE_ANY_EXHDR(p));

    return_result = JS_NewStringLen(ctx, buf, VARSIZE_ANY_EXHDR(p));
    pfree(buf);
    break;
  }

  case DATEOID:
    return_result =
        JS_NewDate(ctx, pljs_convert_date_to_epoch(DatumGetDateADT(arg)));
    break;
  case TIMESTAMPOID:
  case TIMESTAMPTZOID:
    return_result = JS_NewDate(
        ctx, pljs_convert_timestamptz_to_epoch(DatumGetTimestampTz(arg)));
    break;

  default:
    return_result = pljs_datum_to_jsvalue_fallback(arg, type, ctx);
  }

  return return_result;
}

/**
 * @brief Converts a Javascript array to a Postgres array.
 *
 * Takes a Javascript #JSValue of an array and type and converts
 * it into a Postgres array.
 *
 * @param type #pljs_type - type information for the array
 * @param val #JSValue - Javascript array to convert
 * @param ctx #JSContext - Javascript context to execute in
 * @param fcinfo #FunctionCallInfo - needed to conversion back to a #Datum
 * @returns #Datum of the array
 */
Datum pljs_jsvalue_to_array(pljs_type *type, JSValue val, JSContext *ctx,
                            FunctionCallInfo fcinfo) {
  ArrayType *result;
  Datum *values;
  bool *nulls;
  int ndims[1];
  int lbs[] = {[0] = 1};

  int32_t array_length = pljs_js_array_length(val, ctx);

  values = (Datum *)palloc(sizeof(Datum) * array_length);
  nulls = (bool *)palloc(sizeof(bool) * array_length);

  memset(nulls, 0, sizeof(bool) * array_length);

  ndims[0] = array_length;

  for (int i = 0; i < array_length; i++) {
    JSValue elem = JS_GetPropertyUint32(ctx, val, i);

    if (JS_IsNull(elem)) {
      nulls[i] = true;
    } else {
      values[i] =
          pljs_jsvalue_to_datum(type->typid, elem, &nulls[i], ctx, fcinfo);
    }
  }

  result = construct_md_array(values, nulls, 1, ndims, lbs, type->typid,
                              type->length, type->byval, type->align);
  pfree(values);
  pfree(nulls);

  return PointerGetDatum(result);
}

/**
 * @brief Determines whether a Javascript object contains all of the
 * column names.
 *
 * Takes a Javascript #JSValue object and the possible column names
 * and determines whether all of the column names are reflected in
 * the object.
 *
 * @param val #JSValue - Javascript object to check
 * @param ctx #JSContext - Javascript context to execute in
 * @oaram tupdesc #TupleDesc
 * @returns @c bool
 */
bool pljs_jsvalue_object_contains_all_column_names(JSValue val, JSContext *ctx,
                                                   TupleDesc tupdesc) {
  uint32_t object_keys_length = 0;
  JSPropertyEnum *tab;

  if (JS_GetOwnPropertyNames(ctx, &tab, &object_keys_length, val,
                             JS_GPN_STRING_MASK) < 0) {
    return false;
  }

  for (int16 c = 0; c < tupdesc->natts; c++) {
    if (TupleDescAttr(tupdesc, c)->attisdropped) {
      continue;
    }

    char *colname = NameStr(TupleDescAttr(tupdesc, c)->attname);

    // Check to see if the key exists in the object
    bool found = false;
    for (uint32_t object_key = 0; object_key < object_keys_length;
         object_key++) {
      const char *atom = JS_AtomToCString(ctx, tab[object_key].atom);

      if (strcmp(colname, atom) == 0) {
        found = true;
        JS_FreeCString(ctx, atom);
        break;
      }

      JS_FreeCString(ctx, atom);
    }

    if (!found) {
      return false;
    }
  }

  return true;
}

/**
 * @brief Converts a composite Javascript object into an array of Datums.
 *
 * Takes a Javascript object and converts it into an array of Datums, setting
 * the null flag for each Datum if it is null.  Note that this function assumes
 * that `is_null` is allocated and initialized to `0` (`false`) for each
 * element.
 *
 * @param type #pljs_type - type information for the record (used if tupdesc is
 * NULL)
 * @param val #JSValue - the Javascript object to convert
 * @param is_null @c bool** - pointer to array of null flags for each element
 * @param tupdesc #TupleDesc - can be `NULL`, will be looked up from type if so
 * @param ctx #JSContext - Javascript context to execute in
 * @returns Array of #Datum of the Javascript object, or NULL if val is
 * null/undefined
 */
Datum *pljs_jsvalue_to_datums(pljs_type *type, JSValue val, bool **is_null,
                              TupleDesc tupdesc, JSContext *ctx) {
  // Check for null/undefined BEFORE any allocations to avoid memory leaks
  if (JS_IsNull(val) || JS_IsUndefined(val)) {
    return NULL;
  }

  // Get the tuple descriptor, looking it up if not provided
  bool cleanup_tupdesc;
  tupdesc = pljs_get_tupdesc(type ? type->typid : InvalidOid, tupdesc,
                             &cleanup_tupdesc);

  // Allocate the values array now that we have the tuple descriptor
  Datum *values = (Datum *)palloc(sizeof(Datum) * tupdesc->natts);

  for (int16 c = 0; c < tupdesc->natts; c++) {
    // If this is a dropped column, we can skip it, and set the null flag to
    // true.
    if (TupleDescAttr(tupdesc, c)->attisdropped) {
      (*is_null)[c] = true;
      continue;
    }

    // Retrieve the column name of each attribute that we are expecting, we
    // only care about named tuples.
    char *colname = NameStr(TupleDescAttr(tupdesc, c)->attname);

    JSValue o = JS_GetPropertyStr(ctx, val, colname);

    if (JS_IsNull(o) || JS_IsUndefined(o)) {
      (*is_null)[c] = true;
      continue;
    }

    // Set the value of each Datum, or set the `is_null` flag if it is
    // considered `NULL`.
    values[c] = pljs_jsvalue_to_datum(TupleDescAttr(tupdesc, c)->atttypid, o,
                                      &(*is_null)[c], ctx, NULL);
  }

  if (cleanup_tupdesc) {
    ReleaseTupleDesc(tupdesc);
  }

  return values;
}

/**
 * @brief Converts a Javascript object into a Postgres record.
 *
 * Takes a Javascript object and converts it into a Postgres
 * record (composite Postgres type).
 *
 * @param type #pljs_type - type information for the record
 * @param val #JSValue - the Javascript object to convert
 * @param is_null @c bool - pointer to fill of whether the record is null
 * @param tupdesc #TupleDesc - can be `NULL`
 * @param ctx #JSContext - Javascript context to execute in
 * @returns #Datum of the Postgres record
 */
Datum pljs_jsvalue_to_record(pljs_type *type, JSValue val, bool *is_null,
                             TupleDesc tupdesc, JSContext *ctx) {
  Datum result = 0;

  // If the value is null or undefined, we can simply set the record to null
  // and return a `NULL` Datum.
  if (JS_IsNull(val) || JS_IsUndefined(val)) {
    *is_null = true;
    return (Datum)0;
  }

  // Get the tuple descriptor, looking it up if not provided
  bool cleanup_tupdesc;
  tupdesc = pljs_get_tupdesc(type->typid, tupdesc, &cleanup_tupdesc);

  Datum *values = (Datum *)palloc0(sizeof(Datum) * tupdesc->natts);
  bool *nulls = (bool *)palloc0(sizeof(bool) * tupdesc->natts);

  for (int16 c = 0; c < tupdesc->natts; c++) {
    if (TupleDescAttr(tupdesc, c)->attisdropped) {
      nulls[c] = true;
      continue;
    }

    char *colname = NameStr(TupleDescAttr(tupdesc, c)->attname);

    JSValue o = JS_GetPropertyStr(ctx, val, colname);

    if (JS_IsNull(o) || JS_IsUndefined(o)) {
      nulls[c] = true;
      continue;
    }

    values[c] = pljs_jsvalue_to_datum(TupleDescAttr(tupdesc, c)->atttypid, o,
                                      &nulls[c], ctx, NULL);
  }

  // Form a Tuple from the values and nulls using the tuple descriptor
  // as the template for the tuple.
  result = HeapTupleGetDatum(heap_form_tuple(tupdesc, values, nulls));

  pfree(nulls);
  pfree(values);

  if (cleanup_tupdesc) {
    ReleaseTupleDesc(tupdesc);
  }

  return result;
}

/**
 * @brief Fallback type conversion from @JSValue to @Datum.
 *
 * If a type is unknown, instead of returning `NULL`, do a
 * `cstring` or `varlena`, or value based conversion to an `Datum`.
 *
 * In the case of fixed-length `INTERNALLENGTH`, it will be a `cstring`
 * with those bytes copied into the cstring.  When the `String` is variable,
 * a `varlena` will be used and the size set appropriately.
 *
 * @param value #JSValue - Javascript to convert
 * @param is_null @c bool - pointer to fill of whether the value is null
 * @param type #pljs_type - type of the datum
 * @param ctx #JSContext - Javascript context
 * @returns #Datum conversion of the JSValue
 */
static Datum pljs_jsvalue_to_datum_fallback(JSValue value, bool *is_null,
                                            pljs_type type, JSContext *ctx) {
  Datum ret = 0;

  // Set whether the Datum is `NULL` or not.
  JSValue is_set_null_value = JS_GetPropertyStr(ctx, value, "is_null");
  *is_null = JS_ToBool(ctx, is_set_null_value);

  // If the value's property of `null` is set to `true`, we return an empty
  // Datum.
  if (*is_null) {
    return (Datum)0;
  }

  // If the type is by value, it's a 32bit value.
  if (type.byval) {
    int32_t v;
    ret = JS_ToInt32(ctx, &v, value);

    ret = v;
  } else {
    // Get a copy of the data, as well as its length.
    size_t length;
    const char *js_data = JS_ToCStringLen(ctx, &length, value);

    //  If this is a variable length array then we return a `varlena`.
    if (type.length == -1) {
      //  Allocate a new cstring of the length of the type.
      struct varlena *return_data = (struct varlena *)palloc(VARHDRSZ + length);

      // Copy in the data and set the size.
      memcpy(VARDATA(return_data), js_data, length);
      SET_VARSIZE(return_data, length + VARHDRSZ);

      ret = PointerGetDatum(return_data);
    } else if (type.length > 0) {
      // Allocate the memory for the type.
      char *return_data = palloc0(type.length);

      if (length < (size_t)type.length) {
        memcpy(return_data, js_data, length);
      } else {
        memcpy(return_data, js_data, type.length);
      }

      ret = PointerGetDatum(return_data);
    }
  }

  return ret;
}

/**
 * @brief Converts a Javascript value to a Postgres #Datum.
 *
 * Takes a Javascript value and converts it into a Postgres #Datum,
 * checking whether it is an array or record and converting it
 * properly.
 *
 * @param rettype #Oid - type information for the record
 * @param val #JSValue - the Javascript object to convert
 * @param is_null @c bool* - pointer to fill with whether the result is null
 * @param ctx #JSContext - Javascript context to execute in
 * @param fcinfo #FunctionCallInfo - optional, can be NULL
 * @returns #Datum of the Postgres value
 */
Datum pljs_jsvalue_to_datum(Oid rettype, JSValue val, bool *is_null,
                            JSContext *ctx, FunctionCallInfo fcinfo) {
  // Initialize is_null to false
  if (is_null) {
    *is_null = false;
  }

  pljs_type type;

  pljs_type_fill(&type, rettype);

  if (type.typid != JSONOID && type.typid != JSONBOID && JS_IsArray(ctx, val)) {
    return pljs_jsvalue_to_array(&type, val, ctx, fcinfo);
  }

  if (type.category == TYPCATEGORY_ARRAY && !JS_IsArray(ctx, val)) {
    elog(ERROR, "value is not an Array");
  }

  if (type.is_composite) {
    return pljs_jsvalue_to_record(&type, val, is_null, NULL, ctx);
  }

  if (JS_IsNull(val) || JS_IsUndefined(val)) {
    if (fcinfo) {
      PG_RETURN_NULL();
    } else {
      if (is_null) {
        *is_null = true;
      }

      PG_RETURN_NULL();
    }
  }

  switch (rettype) {
  case VOIDOID:
    PG_RETURN_VOID();
    break;

  case OIDOID: {
    int64_t in;
    JS_ToInt64(ctx, &in, val);

    PG_RETURN_OID(in);
    break;
  }

  case BOOLOID: {
    int8_t in = JS_ToBool(ctx, val);
    PG_RETURN_BOOL(in);
    break;
  }

  case INT2OID: {
    int32_t in;
    if (JS_IsBigInt(ctx, val)) {
      int64_t big_in;

      JS_ToBigInt64(ctx, &big_in, val);

      in = (int32_t)big_in;
    } else {
      JS_ToInt32(ctx, &in, val);
    }

    PG_RETURN_INT16((int16_t)in);
    break;
  }

  case INT4OID: {
    int32_t in;
    if (JS_IsBigInt(ctx, val)) {
      int64_t big_in;

      JS_ToBigInt64(ctx, &big_in, val);

      in = (int32_t)big_in;
    } else {
      JS_ToInt32(ctx, &in, val);
    }

    PG_RETURN_INT32(in);
    break;
  }

  case INT8OID: {
    int64_t in;
    if (JS_IsBigInt(ctx, val)) {
      JS_ToBigInt64(ctx, &in, val);
    } else {
      JS_ToInt64(ctx, &in, val);
    }

    PG_RETURN_INT64(in);
    break;
  }

  case FLOAT4OID: {
    double in;
    JS_ToFloat64(ctx, &in, val);

    PG_RETURN_FLOAT4((float4)in);
    break;
  }

  case FLOAT8OID: {
    double in;
    JS_ToFloat64(ctx, &in, val);

    PG_RETURN_FLOAT8(in);
    break;
  }

  case NUMERICOID: {
    if (JS_IsBigInt(ctx, val)) {
      // Convert the value to a string then convert it to NUMERIC.
      JSValue str = JS_ToString(ctx, val);

      const char *in = JS_ToCString(ctx, str);

      return DirectFunctionCall3(numeric_in, (Datum)in,
                                 ObjectIdGetDatum(InvalidOid),
                                 Int32GetDatum((int32)-1));

    } else {
      double in;

      JS_ToFloat64(ctx, &in, val);

      return DirectFunctionCall1(float8_numeric, Float8GetDatum((float8)in));
    }
    break;
  }

  case TEXTOID:
  case VARCHAROID:
  case BPCHAROID:
  case NAMEOID:
  case XMLOID: {
    size_t plen;
    const char *str = JS_ToCStringLen(ctx, &plen, val);

    Datum ret = CStringGetTextDatum(str);
    JS_FreeCString(ctx, str);

    return ret;
    break;
  }

  case JSONOID: {
    JSValueConst *argv = &val;
    JSValue js = JS_JSONStringify(ctx, argv[0], JS_UNDEFINED, JS_UNDEFINED);
    size_t plen;
    const char *str = JS_ToCStringLen(ctx, &plen, js);

    // return it as a CStringTextDatum.
    Datum ret = CStringGetTextDatum(str);

    JS_FreeCString(ctx, str);
    JS_FreeValue(ctx, js);

    return ret;
    break;
  }

  case JSONBOID: {
    JSValueConst *argv = &val;
#if JSONB_DIRECT_CONVERSION
    {
      Jsonb *obj = convert_object(argv[0], ctx);
      PG_RETURN_JSONB_P(DatumGetJsonbP((unsigned long)obj));
    }
#else // JSONB_DIRECT_CONVERSION
    JSValue js = JS_JSONStringify(ctx, argv[0], JS_UNDEFINED, JS_UNDEFINED);

    const char *str = JS_ToCString(ctx, js);

    // return it as a Datum, since there is no direct CStringGetJsonb exposed.
    Datum ret = (Datum)DatumGetJsonbP(
        DirectFunctionCall1(jsonb_in, (Datum)(char *)str));

    JS_FreeCString(ctx, str);
    JS_FreeValue(ctx, js);

    return ret;
#endif
    break;
  }

  case BYTEAOID: {
    size_t psize;
    size_t pbytes_per_element = 0;

    uint8_t *buffer;

    uint32_t length = pljs_js_array_length(val, ctx);

    if (Is_ArrayType(val, JS_CLASS_UINT8_ARRAY) ||
        Is_ArrayType(val, JS_CLASS_INT8_ARRAY)) {
      pbytes_per_element = 1;
      psize = pbytes_per_element * length;

      uint8_t *array_copy = palloc(pbytes_per_element * length);

      for (size_t i = 0; i < length; i++) {
        int32_t in;

        JSValue jsval = JS_GetPropertyUint32(ctx, val, i);
        JS_ToInt32(ctx, &in, jsval);
        array_copy[i] = (uint8_t)in;
      }

      buffer = palloc(VARHDRSZ + psize);

      SET_VARSIZE(buffer, psize + VARHDRSZ);
      memcpy(VARDATA(buffer), array_copy, psize);

      pfree(array_copy);

      return PointerGetDatum(buffer);
    } else if (Is_ArrayType(val, JS_CLASS_UINT16_ARRAY) ||
               Is_ArrayType(val, JS_CLASS_INT16_ARRAY)) {
      pbytes_per_element = 2;
      psize = pbytes_per_element * length;

      uint16_t *array_copy = palloc(pbytes_per_element * length);

      for (size_t i = 0; i < length; i++) {
        int32_t in;

        JSValue jsval = JS_GetPropertyUint32(ctx, val, i);
        JS_ToInt32(ctx, &in, jsval);
        array_copy[i] = (uint16_t)in;
      }

      buffer = palloc(VARHDRSZ + psize);

      SET_VARSIZE(buffer, psize + VARHDRSZ);
      memcpy(VARDATA(buffer), array_copy, psize);

      pfree(array_copy);

      return PointerGetDatum(buffer);
    } else if (Is_ArrayType(val, JS_CLASS_UINT32_ARRAY) ||
               Is_ArrayType(val, JS_CLASS_INT32_ARRAY)) {
      pbytes_per_element = 4;
      psize = pbytes_per_element * length;

      uint32_t *array_copy = palloc(pbytes_per_element * length);

      for (size_t i = 0; i < pbytes_per_element * length; i++) {
        int32_t in;

        JSValue jsval = JS_GetPropertyUint32(ctx, val, i);
        JS_ToInt32(ctx, &in, jsval);
        array_copy[i] = (uint32_t)in;
      }

      buffer = palloc(VARHDRSZ + psize);

      SET_VARSIZE(buffer, psize + VARHDRSZ);
      memcpy(VARDATA(buffer), array_copy, psize);

      pfree(array_copy);

      return PointerGetDatum(buffer);

    } else if (Is_ArrayBuffer(val)) {
      uint8_t *array_copy = JS_GetArrayBuffer(ctx, &psize, val);

      buffer = palloc(VARHDRSZ + psize);

      SET_VARSIZE(buffer, psize + VARHDRSZ);
      memcpy(VARDATA(buffer), array_copy, psize);
      return PointerGetDatum(buffer);
    } else if (JS_IsString(val)) {
      size_t str_length;
      const char *str = JS_ToCStringLen(ctx, &str_length, val);

      buffer = palloc(str_length + VARHDRSZ);

      SET_VARSIZE(buffer, str_length + VARHDRSZ);
      memcpy(VARDATA(buffer), str, str_length);

      JS_FreeCString(ctx, str);

      return PointerGetDatum(buffer);
    } else {
      elog(DEBUG3, "Unknown array type, tag: %lld", val.tag);
      for (uint8_t i = 0; i < 255; i++) {
        void *res = JS_GetOpaque(val, i);
        if (res != NULL) {
          elog(DEBUG3, "class_id: %d", i);
        }
      }

      PG_RETURN_NULL();
    }
  }

  case DATEOID:
    if (Is_Date(val)) {
      double in;
      JS_ToFloat64(ctx, &in, val);
      return pljs_convert_epoch_to_date(in);
    }
    break;
  case TIMESTAMPOID:
  case TIMESTAMPTZOID:
    if (Is_Date(val)) {
      double in;
      JS_ToFloat64(ctx, &in, val);
      return pljs_convert_epoch_to_timestamptz(in);
    }
    break;

  default:
    return pljs_jsvalue_to_datum_fallback(val, is_null, type, ctx);
  }

  // shut up, compiler
  PG_RETURN_NULL();
}

/**
 * @brief Converts an array of Javascript values into a Javascript array.
 *
 * Takes an array Javascript values and converts it into a Javascript
 * array of values, starting at the index requested.
 *
 * @param array #JSValue - array of #JSValue values to convert
 * @param argc @c int - number of values to convert
 * @param start @c int - index to start the conversion
 * @param ctx #JSContext - Javascript context to execute in
 * @returns #JSValue array of the results
 */
JSValue pljs_values_to_array(JSValue *array, int argc, int start,
                             JSContext *ctx) {
  JSValue ret = JS_NewArray(ctx);

  uint32_t current = 0;
  for (int i = start; i < argc; i++) {
    JS_SetPropertyUint32(ctx, ret, current, array[i]);
    current++;
  }

  return ret;
}

/**
 * @brief Converts a Postgres #HeapTuple to a Javascript value.
 *
 * @param tupledesc #TupleDesc
 * @param heap_tuple #HeapTuple - value to convert
 * @param ctx #JSContext - Javascript context to execute in
 * @returns #JSValue of the tuple value passed
 */
JSValue pljs_tuple_to_jsvalue(TupleDesc tupledesc, HeapTuple heap_tuple,
                              JSContext *ctx) {
  JSValue obj = JS_NewObject(ctx);

  for (int i = 0; i < tupledesc->natts; i++) {
    FormData_pg_attribute *tuple_attrs = TupleDescAttr(tupledesc, i);
    if (tuple_attrs->attisdropped) {
      continue;
    }

    bool isnull;
    Datum datum = heap_getattr(heap_tuple, i + 1, tupledesc, &isnull);

    char *name = NameStr(tuple_attrs->attname);

    JS_SetPropertyStr(
        ctx, obj, name,
        pljs_datum_to_jsvalue(tuple_attrs->atttypid, datum, isnull, true, ctx));
  }

  return obj;
}

/**
 * @brief Converts a Postgres SPI result to a Javascript value.
 *
 * @param status @c int - SPI status to convert
 * @param ctx #JSContext - Javascript context to execute in
 * @returns #JSValue of the SPI status
 */
JSValue pljs_spi_result_to_jsvalue(int status, JSContext *ctx) {
  JSValue result;

  if (status < 0) {
    return js_throw(pljs_util_spi_status_string(status), ctx);
  }

  switch (status) {
  case SPI_OK_UTILITY:
  case SPI_OK_REWRITTEN:
    if (SPI_tuptable == NULL) {
      result = JS_NewInt32(ctx, SPI_processed);
      break;
    }
    // will fallthrough here to the "SELECT" logic below

  case SPI_OK_SELECT:
  case SPI_OK_INSERT_RETURNING:
  case SPI_OK_DELETE_RETURNING:
  case SPI_OK_UPDATE_RETURNING: {
    int nrows = SPI_processed;
    TupleDesc tupdesc = SPI_tuptable->tupdesc;

    JSValue obj = JS_NewArray(ctx);

    for (int r = 0; r < nrows; r++) {
      JSValue value =
          pljs_tuple_to_jsvalue(tupdesc, SPI_tuptable->vals[r], ctx);

      JS_SetPropertyUint32(ctx, obj, r, value);
    }

    result = obj;
    break;
  }
  default:
    result = JS_NewInt32(ctx, SPI_processed);
    break;
  }

  return result;
}

#if JSONB_DIRECT_CONVERSION
/**
 * @brief Converts a #JsonbValue to a #JSValue.
 *
 * @param scalar_value #JsonbValue - value to convert
 * @param ctx #JSContext - Javascript context to execute in
 * @returns #JSValue of the #JsonbValue
 */
static JSValue get_jsonb_value(JsonbValue *scalar_value, JSContext *ctx) {
  // If the value is `null` then we return `null`.
  if (scalar_value->type == jbvNull) {
    return JS_NULL;
  } else if (scalar_value->type == jbvString) {
    // A `String`.
    return JS_NewStringLen(ctx, scalar_value->val.string.val,
                           scalar_value->val.string.len);
  } else if (scalar_value->type == jbvNumeric) {
    // `Number`.
    return JS_NewFloat64(
        ctx, DatumGetFloat8(DirectFunctionCall1(
                 numeric_float8, PointerGetDatum(scalar_value->val.numeric))));
  } else if (scalar_value->type == jbvBool) {
    // `Bool`.
    return JS_NewBool(ctx, scalar_value->val.boolean);
  } else {
    elog(ERROR, "unknown jsonb scalar type");
    return JS_NULL;
  }
}

/**
 * @brief Iterates through a #JsonbIterator.
 *
 * Iterate through a `JSONB` object and creates the proper Javascript type
 * for each: `Number`, `String`, `Bool`, `Date`, `Array`, `Object`.  This
 * function is meant to be run recursively.
 *
 * @param it #JsonbIterator - `JSONB` iterator to iterate on
 * @param container #JSValue - parent container to store the value in
 * @param ctx #JSContext - Javascript context to execute in
 * @returns #JSValue of `JSONB` value
 */
static JSValue jsonb_iterate(JsonbIterator **it, JSValue container,
                             JSContext *ctx) {
  JsonbValue value;
  int32 count = 0;
  JsonbIteratorToken token;
  JSValue key;
  char *key_string = NULL;
  JSValue obj;

  // Get the next value from the `JSONB` object.
  token = JsonbIteratorNext(it, &value, false);

  // Iterate through the values until the end of the `JSONB` object.
  while (token != WJB_DONE) {
    switch (token) {
    // If it is a new Object, create one.
    case WJB_BEGIN_OBJECT:
      obj = JS_NewObject(ctx);

      // If our container is an `Array`, append the object.
      // Iterate through the `JSONB` array until we get to the end of the array.
      if (JS_IsArray(ctx, container)) {
        JS_SetPropertyUint32(ctx, container, count,
                             jsonb_iterate(it, obj, ctx));
        count++;
      } else {
        // Otherwise set the property of the `Object`.  We use the
        // #key_string that we previously stored from the `JSONB` object.
        // Iterate through the `JSONB` object until we get to the end of the
        // object.
        JS_SetPropertyStr(ctx, container, key_string,
                          jsonb_iterate(it, obj, ctx));
        JS_FreeCString(ctx, key_string);
        key_string = NULL;
      }
      break;

      // If we are done with the object, return the container.
    case WJB_END_OBJECT:
      return container;

      break;

      // Start of a new `Array`.
    case WJB_BEGIN_ARRAY:
      obj = JS_NewArray(ctx);
      if (JS_IsArray(ctx, container)) {
        JS_SetPropertyUint32(ctx, container, count,
                             jsonb_iterate(it, obj, ctx));
        count++;
      } else {
        JS_SetPropertyStr(ctx, container, key_string,
                          jsonb_iterate(it, obj, ctx));
        JS_FreeCString(ctx, key_string);
        key_string = NULL;
      }
      break;

      // End of the array, return the container.
    case WJB_END_ARRAY:
      return container;

      break;

      // Retrieve the key for an object and store it as `key_string`.
    case WJB_KEY:
      key = get_jsonb_value(&value, ctx);
      key_string = (char *)JS_ToCString(ctx, key);
      JS_FreeValue(ctx, key);

      break;

      // Retrieve the object value and set it using `key_string`.
    case WJB_VALUE:
      JS_SetPropertyStr(ctx, container, key_string,
                        get_jsonb_value(&value, ctx));
      JS_FreeCString(ctx, key_string);

      // Clear the `key_string` so it cannot be re-used.
      key_string = NULL;

      break;

      // Retrieve an array element and set it, then increment the count.
    case WJB_ELEM:
      JS_SetPropertyUint32(ctx, container, count, get_jsonb_value(&value, ctx));
      count++;
      break;

      // We are done, return the container.
    case WJB_DONE:
      return container;
      break;

    default:
      elog(ERROR, "unknown jsonb iterator value");
    }

    // Retrieve the next `JSONB` token for the loop.
    token = JsonbIteratorNext(it, &value, false);
  }

  return container;
}

/**
 * @brief Converts a #JsonbContainer to a Javascript value.
 *
 * Entry function for the `JSONB` iterator, sets up a container to
 * eventually be returned, then calls the iterator function to fill the
 * container.
 *
 * @param in #JsonbContainer - the `JSONB` object to convert
 * @param ctx #JSContext - Javascript context to execute in
 * @returns #JSValue of the `JSONB` object
 */
static JSValue convert_jsonb(JsonbContainer *in, JSContext *ctx) {
  JsonbValue val;
  JsonbIterator *it = JsonbIteratorInit(in);
  JsonbIteratorToken token = JsonbIteratorNext(&it, &val, false);

  // `JSONB` objects always need to be an `Array` or `Object`.
  JSValue container;

  // If this is an array, then create an `Array`.
  if (token == WJB_BEGIN_ARRAY) {
    container = JS_NewArray(ctx);
  } else {
    // Otherwise it is an `Object` by default.
    container = JS_NewObject(ctx);
  }

  return jsonb_iterate(&it, container, ctx);
}

// Forward declarations of the conversion functions.
static JsonbValue *jsonb_object_from_object(JSValue object,
                                            JsonbParseState **pstate,
                                            JSContext *ctx);
static JsonbValue *
jsonb_array_from_array(JSValue array, JsonbParseState **pstate, JSContext *ctx);

/**
 * @brief Converts a Postgres time in milliseconds to a 8601 datetime string.
 *
 * @param millis @c double - Postgres time in milliseconds
 * @returns @c char * representation of the date and time
 */
static char *time_as_8601(double millis) {
  char tmp[100];
  char *buf = (char *)palloc(25);

  time_t t = (time_t)(millis / 1000);
  strftime(tmp, 25, "%Y-%m-%dT%H:%M:%S", gmtime(&t));

  double integral;
  double fractional = modf(millis / 1000, &integral);

  sprintf(buf, "%s.%03dZ", tmp, (int)(fractional * 1000));

  return buf;
}

/**
 * @brief Converts a #JSValue into a `JSONB` value.
 *
 * @param parse_state #JsonbParseState - current state of the `JSONB` parsing
 * @param value #JSValue - the value to convert
 * @param type #JsonbIteratorToken
 * @param ctx #JSContext - Javascript context to execute in
 * @returns #JsonbValue `JSONB` result from the conversion
 */
static JsonbValue *jsonb_from_value(JSValue value,
                                    JsonbParseState **parse_state,
                                    JsonbIteratorToken type, JSContext *ctx,
                                    const char *key) {
  JsonbValue val;

  // If the token type is a key, the only valid value is `jbvString`.
  if (type == WJB_KEY) {
    val.type = jbvString;
    size_t len = strlen(key);

    val.val.string.val = palloc(len);
    memcpy(val.val.string.val, key, len);
    val.val.string.len = len;

    JS_FreeCString(ctx, key);
  } else {
    // Otherwise make the conversion based on the #JSValue type.
    if (JS_IsBool(value)) {
      val.type = jbvBool;
      val.val.boolean = JS_ToBool(ctx, value);
    } else if (JS_IsNull(value)) {
      val.type = jbvNull;
    } else if (JS_IsUndefined(value)) {
      return NULL;
    } else if (JS_IsString(value)) {
      val.type = jbvString;
      size_t len;
      const char *v = JS_ToCStringLen(ctx, &len, value);

      val.val.string.val = palloc(len);
      memcpy(val.val.string.val, v, len);
      val.val.string.len = len;

      JS_FreeCString(ctx, v);
    } else if (JS_IsNumber(value)) {
      double in;

      JS_ToFloat64(ctx, &in, value);

      val.val.numeric = DatumGetNumeric(
          DirectFunctionCall1(float8_numeric, Float8GetDatum((float8)in)));
      val.type = jbvNumeric;
    } else if (Is_Date(value)) {
      double in;

      JS_ToFloat64(ctx, &in, value);

      if (isnan(in)) {
        val.type = jbvNull;
      } else {
        val.val.string.val = time_as_8601(in);
        val.val.string.len = 24;
        val.type = jbvString;
      }
    } else {
      val.type = jbvString;
      size_t len;
      const char *v = JS_ToCStringLen(ctx, &len, value);

      val.val.string.val = palloc(len);
      memcpy(val.val.string.val, v, len);
      val.val.string.len = len;

      JS_FreeCString(ctx, v);
    }
  }

  // Push the result into the parse_state.
  return pushJsonbValue(parse_state, type, &val);
}

/**
 * @brief Converts a #JSValue `Array` to a #JsonbValue array.
 *
 * @param array #JSValue - `Array` to convert
 * @param parse_state #JsonbParseState - the parse state of the `JSONB` object
 * @param ctx #JSContext - Javascript context to execute in
 * @returns #JsonbValue of the `JSONB` array
 */
static JsonbValue *jsonb_array_from_array(JSValue array,
                                          JsonbParseState **parse_state,
                                          JSContext *ctx) {
  // Push the beginning of the array into the parse state.
  JsonbValue *value = pushJsonbValue(parse_state, WJB_BEGIN_ARRAY, NULL);

  // Get the length of the `Array`.
  int32_t array_length = pljs_js_array_length(array, ctx);

  // Iterate through the `Array`.
  for (int i = 0; i < array_length; i++) {
    // Get the current element.
    JSValue elem = JS_GetPropertyUint32(ctx, array, i);

    // For each type, set `value` to the result.
    if (JS_IsArray(ctx, elem)) {
      value = jsonb_array_from_array(elem, parse_state, ctx);
    } else if (JS_IsObject(elem)) {
      value = jsonb_object_from_object(elem, parse_state, ctx);
    } else {
      value = jsonb_from_value(elem, parse_state, WJB_ELEM, ctx, NULL);
    }

    // Free up the element.
    JS_FreeValue(ctx, elem);
  }

  // Set the value to the end of the array.
  value = pushJsonbValue(parse_state, WJB_END_ARRAY, NULL);

  return value;
}

/**
 * @brief Converts a #JSValue `Object` to a #JsonbValue object.
 *
 * @param object #JSValue - `Object` to convert
 * @param parse_state #JsonbParseState - the parse state of the `JSONB` object
 * @param ctx #JSContext - Javascript context to execute in
 * @returns #JsonbValue of the `JSONB` object
 */
static JsonbValue *jsonb_object_from_object(JSValue object,
                                            JsonbParseState **parse_state,
                                            JSContext *ctx) {
  // Push the beginning of the object intp the parse state.
  JsonbValue *value = pushJsonbValue(parse_state, WJB_BEGIN_OBJECT, NULL);
  uint32_t object_keys_length = 0;
  JSPropertyEnum *tab;

  // Get the keys of the `Object`.
  if (JS_GetOwnPropertyNames(ctx, &tab, &object_keys_length, object,
                             JS_GPN_STRING_MASK) < 0) {
    return false;
  }

  // Iterate through the `Object` keys.
  for (uint32_t object_key = 0; object_key < object_keys_length; object_key++) {
    // Get the value.
    JSValue o =
        JS_GetPropertyInternal(ctx, object, tab[object_key].atom, object, 0);

    const char *key = JS_AtomToCString(ctx, tab[object_key].atom);

    value = jsonb_from_value(o, parse_state, WJB_KEY, ctx, key);

    // If the value is an `Array` the convert it.
    if (JS_IsArray(ctx, o)) {
      value = jsonb_array_from_array(o, parse_state, ctx);
    } else if (JS_IsObject(o)) {
      // Or convert an `Object`.
      value = jsonb_object_from_object(o, parse_state, ctx);
    } else {
      // Or anything else.
      value = jsonb_from_value(o, parse_state, WJB_VALUE, ctx, NULL);
    }

    // Free up the memory.
    JS_FreeValue(ctx, o);
  }

  // Push that we are at the end of an object.
  value = pushJsonbValue(parse_state, WJB_END_OBJECT, NULL);

  return value;
}

/**
 * @brief Converts a #JSValue `Object` to a #Jsonb value.
 *
 * @param object #JSValue - `Object` to convert
 * @param ctx #JSContext - Javascript context to execute in
 * @returns #Jsonb the converted `JSONB` value
 */
static Jsonb *convert_object(JSValue object, JSContext *ctx) {
  // Create a new memory context for conversion.
  MemoryContext oldcontext = CurrentMemoryContext;
  MemoryContext conversion_context;
  conversion_context = AllocSetContextCreate(
      CurrentMemoryContext, "JSONB Conversion Context", ALLOCSET_SMALL_SIZES);

  MemoryContextSwitchTo(conversion_context);

  JsonbParseState *parse_state = NULL;
  JsonbValue *value;

  // Check the type and get its value.
  if (JS_IsArray(ctx, object)) {
    value = jsonb_array_from_array(object, &parse_state, ctx);
  } else if (JS_IsObject(object)) {
    value = jsonb_object_from_object(object, &parse_state, ctx);
  } else {
    pushJsonbValue(&parse_state, WJB_BEGIN_ARRAY, NULL);
    jsonb_from_value(object, &parse_state, WJB_ELEM, ctx, NULL);
    value = pushJsonbValue(&parse_state, WJB_END_ARRAY, NULL);
    value->val.array.rawScalar = true;
  }

  // Switch back to our old #MemoryContext.
  MemoryContextSwitchTo(oldcontext);

  // Create the #Jsonb object to return.
  Jsonb *ret = JsonbValueToJsonb(value);

  // Delete the conversion #MemoryContext.
  MemoryContextDelete(conversion_context);

  return ret;
}
#endif
