#include "jsffi.h"
#include "font.h"
#include "datastream.h"
#include "qjs_sdl.h"
#include "qjs_sdl_input.h"
#include "transform.h"
#include "stb_ds.h"
#include "stb_image.h"
#include "stb_rect_pack.h"
#include "string.h"
#include <assert.h>
#include <time.h>
#include <sys/time.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <unistd.h>
#include <limits.h>
#include "render.h"
#include "HandmadeMath.h"
#include "par/par_streamlines.h"
#include "par/par_shapes.h"
#include <stdint.h>
#include "cute_aseprite.h"
#include "cell.h"

#include "qjs_blob.h"
#include "qjs_dmon.h"
#include "qjs_enet.h"
#include "qjs_nota.h"
#include "qjs_wota.h"
#include "qjs_soloud.h"
#ifdef HAVE_QRENCODE
#include "qjs_qr.h"
#endif
#include "qjs_sdl.h"
#include "qjs_sdl_video.h"
#include "qjs_math.h"
#include "qjs_geometry.h"
#include "qjs_transform.h"
#include "qjs_sprite.h"
#include "qjs_io.h"
#include "qjs_sdl_gpu.h"
#include "qjs_os.h"
#include "qjs_actor.h"
#include "qjs_rtree.h"
#include "qjs_spline.h"
#include "qjs_js.h"
#include "qjs_debug.h"
#include "qjs_sdl_surface.h"
#include "qjs_sdl.h"
#include "qjs_kim.h"
#include "qjs_utf8.h"
#include "qjs_fit.h"
#include "qjs_text.h"
#include "qjs_staef.h"
#include "qjs_io.h"
#include "qjs_fd.h"
#ifndef NSTEAM
#include "qjs_steam.h"
#endif

// External transform function declarations
extern JSClassID js_transform_id;
JSValue transform2js(JSContext *js, transform *t);
transform *js2transform(JSContext *js, JSValue v);

// Random number generation constants for MT19937-64
#define NN STATE_VECTOR_LENGTH
#define MM STATE_VECTOR_M
#define MATRIX_A 0xB5026F5AA96619E9ULL
#define UM 0xFFFFFFFF80000000ULL /* Most significant 33 bits */
#define LM 0x7FFFFFFFULL /* Least significant 31 bits */

// Random number generation functions
void m_seedRand(MTRand* rand, uint64_t seed) {
  rand->mt[0] = seed;
  for(rand->index = 1; rand->index < NN; rand->index++) {
    rand->mt[rand->index] = (6364136223846793005ULL * (rand->mt[rand->index-1] ^ (rand->mt[rand->index-1] >> 62)) + rand->index);
  }
}

int64_t genRandLong(MTRand* rand) {
  int i;
  uint64_t x;
  static uint64_t mag01[2] = {0ULL, MATRIX_A};

  if (rand->index >= NN) { /* generate NN words at one time */
    /* if init_genrand64() has not been called, */
    /* a default initial seed is used */
    if (rand->index == NN+1) 
      m_seedRand(rand, 5489ULL); 

    for (i = 0; i < NN-MM; i++) {
      x = (rand->mt[i] & UM) | (rand->mt[i+1] & LM);
      rand->mt[i] = rand->mt[i+MM] ^ (x>>1) ^ mag01[(int)(x&1ULL)];
    }
    for (; i < NN-1; i++) {
      x = (rand->mt[i] & UM) | (rand->mt[i+1] & LM);
      rand->mt[i] = rand->mt[i+(MM-NN)] ^ (x>>1) ^ mag01[(int)(x&1ULL)];
    }
    x = (rand->mt[NN-1] & UM) | (rand->mt[0] & LM);
    rand->mt[NN-1] = rand->mt[MM-1] ^ (x>>1) ^ mag01[(int)(x&1ULL)];

    rand->index = 0;
  }
  
  x = rand->mt[rand->index++];

  x ^= (x >> 29) & 0x5555555555555555ULL;
  x ^= (x << 17) & 0x71D67FFFEDA60000ULL;
  x ^= (x << 37) & 0xFFF7EEE000000000ULL;
  x ^= (x >> 43);

  return (int64_t)(x & 0x000FFFFFFFFFFFFFULL); /* return 52-bit value safe for JS */
}

double genRand(MTRand* rand) {
  /* generates a random number on [0,1)-real-interval */
  return (genRandLong(rand) >> 11) * (1.0/9007199254740992.0);
}

double rand_range(JSContext *js, double min, double max)
{
  MTRand *mrand = &((cell_rt*)JS_GetContextOpaque(js))->mrand;
  return genRand(mrand) * (max-min)+min;
}

typedef struct texture_vertex {
  float x, y, z;
  float u, v;
  uint8_t r, g, b,a;
} texture_vertex;

#define JS_GETNUM(JS,VAL,I,TO,TYPE) { \
JSValue val = JS_GetPropertyUint32(JS,VAL,I); \
TO = js2##TYPE(JS, val); \
JS_FreeValue(JS, val); } \

int js2bool(JSContext *js, JSValue v) { return JS_ToBool(js,v); }

JSValue number2js(JSContext *js, double g) { return JS_NewFloat64(js,g); }
double js2number(JSContext *js, JSValue v) {
  double g;
  JS_ToFloat64(js, &g, v);
  if (isnan(g)) g = 0;
  return g;
}

JSValue js_getpropertyuint32(JSContext *js, JSValue v, unsigned int i)
{
  JSValue ret = JS_GetPropertyUint32(js,v,i);
  JS_FreeValue(js,ret);
  return ret;
}

double js_getnum_uint32(JSContext *js, JSValue v, unsigned int i)
{
  JSValue val = JS_GetPropertyUint32(js,v,i);
  double ret = js2number(js, val);
  JS_FreeValue(js,val);
  return ret;
}

double js_getnum_str(JSContext *js, JSValue v, const char *str)
{
  JSValue val = JS_GetPropertyStr(js,v,str);
  double ret = js2number(js,val);
  JS_FreeValue(js,val);
  return ret;
}

#define JS_GETPROP(JS, TARGET, VALUE, PROP, TYPE) {\
JSValue __##PROP##__v = JS_GetPropertyStr(JS,VALUE,#PROP); \
TARGET = js2##TYPE(JS, __##PROP##__v); \
JS_FreeValue(JS,__##PROP##__v); }\

#define JS_GETATOM(JS, TARGET, VALUE, ATOM, TYPE) {\
JSValue __##PROP##__v = JS_GetPropertyStr(JS,VALUE,#ATOM); \
TARGET = js2##TYPE(JS, __##PROP##__v); \
JS_FreeValue(JS,__##PROP##__v); }\

int JS_GETBOOL(JSContext *js, JSValue v, const char *prop)
{
  JSValue __v = JS_GetPropertyStr(js,v,prop);
  int r = JS_ToBool(js,__v);
  JS_FreeValue(js,__v);
  return r;
}

JSValue js_getproperty(JSContext *js, JSValue v, const char *prop)
{
  JSValue ret = JS_GetPropertyStr(js, v, prop);
  JS_FreeValue(js,ret);
  return ret;
}

JSValue make_quad_indices_buffer(JSContext *js, int quads)
{
  cell_rt *rt = JS_GetContextOpaque(js);
  int count = quads*6;
  if (!JS_IsNull(rt->idx_buffer) && rt->idx_count >= count)
    return JS_DupValue(js,rt->idx_buffer);

  int verts = quads*4;
  uint16_t *indices = malloc(sizeof(*indices)*count);
  for (int i = 0, v = 0; v < verts; i +=6, v += 4) {
    indices[i] = v;
    indices[i+1] = v+2;
    indices[i+2] = v+1;
    indices[i+3] = v+2;
    indices[i+4] = v+3;
    indices[i+5] = v+1;
  }

  if (!JS_IsNull(rt->idx_buffer))
    JS_FreeValue(js,rt->idx_buffer);
    
//  rt->idx_buffer = make_gpu_buffer(js,indices, sizeof(*indices)*count, JS_TYPED_ARRAY_UINT16, 1,0,1);
  rt->idx_count = count;
  return JS_DupValue(js,rt->idx_buffer);
}

JSValue quads_to_mesh(JSContext *js, text_vert *buffer)
{
  size_t verts = arrlen(buffer);

  JSValue ret = JS_NewObject(js);
  
  // Allocate flat arrays for xy, uv, and color data
  size_t xy_size = verts * 2 * sizeof(float);
  size_t uv_size = verts * 2 * sizeof(float);
  size_t color_size = verts * sizeof(SDL_FColor);
  
  float *xy_data = malloc(xy_size);
  float *uv_data = malloc(uv_size);
  SDL_FColor *color_data = malloc(color_size);

  // Convert vertex data to flat arrays
  for (int i = 0; i < verts; i++) {
    xy_data[i*2] = buffer[i].pos.x;
    xy_data[i*2+1] = buffer[i].pos.y;
    
    uv_data[i*2] = buffer[i].uv.x;
    uv_data[i*2+1] = buffer[i].uv.y;
    
    color_data[i].r = buffer[i].color.x;
    color_data[i].g = buffer[i].color.y;
    color_data[i].b = buffer[i].color.z;
    color_data[i].a = buffer[i].color.w;
  }

  size_t quads = verts/4;
  size_t count = quads*6;
  
  // Create indices
  uint16_t *indices = malloc(sizeof(uint16_t)*count);
  for (int i = 0, v = 0; i < count; i += 6, v += 4) {
    indices[i] = v;
    indices[i+1] = v+2;
    indices[i+2] = v+1;
    indices[i+3] = v+2;
    indices[i+4] = v+3;
    indices[i+5] = v+1;
  }

  // Create blobs for geometry data
  JS_SetPropertyStr(js, ret, "xy", js_new_blob_stoned_copy(js, xy_data, xy_size));
  JS_SetPropertyStr(js, ret, "xy_stride", JS_NewInt32(js, 2 * sizeof(float)));
  JS_SetPropertyStr(js, ret, "uv", js_new_blob_stoned_copy(js, uv_data, uv_size));
  JS_SetPropertyStr(js, ret, "uv_stride", JS_NewInt32(js, 2 * sizeof(float)));
  JS_SetPropertyStr(js, ret, "color", js_new_blob_stoned_copy(js, color_data, color_size));
  JS_SetPropertyStr(js, ret, "color_stride", JS_NewInt32(js, sizeof(SDL_FColor)));
  JS_SetPropertyStr(js, ret, "indices", js_new_blob_stoned_copy(js, indices, sizeof(uint16_t)*count));
  JS_SetPropertyStr(js, ret, "num_vertices", JS_NewInt32(js, verts));
  JS_SetPropertyStr(js, ret, "num_indices", JS_NewInt32(js, count));
  JS_SetPropertyStr(js, ret, "size_indices", JS_NewInt32(js, 2)); // uint16_t size
  
  free(xy_data);
  free(uv_data);
  free(color_data);
  free(indices);
  
  return ret;
}

typedef struct lrtb lrtb;

lrtb js2lrtb(JSContext *js, JSValue v)
{
  lrtb ret = {0};
  JS_GETATOM(js,ret.l,v,l,number)
  JS_GETATOM(js,ret.r,v,r,number)
  JS_GETATOM(js,ret.b,v,b,number)
  JS_GETATOM(js,ret.t,v,t,number)
  return ret;
}

JSValue vec22js(JSContext *js,HMM_Vec2 v)
{
  JSValue array = JS_NewArray(js);
  JS_SetPropertyUint32(js, array,0,number2js(js,v.x));
  JS_SetPropertyUint32(js, array,1,number2js(js,v.y));
  return array;
}

char *js2strdup(JSContext *js, JSValue v) {
  const char *str = JS_ToCString(js, v);
  char *ret = strdup(str);
  JS_FreeCString(js, str);
  return ret;
}

#include "qjs_macros.h"

QJSCLASS(datastream,)

JSValue angle2js(JSContext *js,double g) {
  return number2js(js,g*HMM_RadToTurn);
}

double js2angle(JSContext *js,JSValue v) {
  double n = js2number(js,v);
  return n * HMM_TurnToRad;
}

typedef HMM_Vec4 colorf;

colorf js2color(JSContext *js,JSValue v) {
  if (JS_IsNull(v)) return (colorf){1,1,1,1};
  
  colorf color = {1,1,1,1}; // Default to white
  
  if (JS_IsArray(js, v)) {
    // Handle array format: [r, g, b, a]
    JSValue c[4];
    for (int i = 0; i < 4; i++) c[i] = JS_GetPropertyUint32(js,v,i);
    
    color.r = js2number(js,c[0]);
    color.g = js2number(js,c[1]);
    color.b = js2number(js,c[2]);
    color.a = JS_IsNull(c[3]) ? 1.0 : js2number(js,c[3]);
    
    for (int i = 0; i < 4; i++) JS_FreeValue(js,c[i]);
  } else if (JS_IsObject(v)) {
    JS_GETPROP(js, color.r, v, r, number)
    JS_GETPROP(js, color.g, v, g, number)
    JS_GETPROP(js, color.b, v, b, number)
    JS_GETPROP(js, color.a, v, a, number)
  }

  return color;
}

JSValue color2js(JSContext *js, colorf color)
{
  JSValue arr = JS_NewArray(js);
  JS_SetPropertyUint32(js, arr,0,number2js(js,(double)color.r));
  JS_SetPropertyUint32(js, arr,1,number2js(js,(double)color.g));  
  JS_SetPropertyUint32(js, arr,2,number2js(js,(double)color.b));
  JS_SetPropertyUint32(js, arr,3,number2js(js,(double)color.a));
  return arr;
}

HMM_Vec2 js2vec2(JSContext *js,JSValue v)
{
  HMM_Vec2 v2;
  
  // Check if it's an array
  if (JS_IsArray(js, v)) {
    v2.X = js_getnum_uint32(js,v,0);
    v2.Y = js_getnum_uint32(js,v,1);
  } else {
    // Try to get x,y properties from object
    JSValue x_val = JS_GetPropertyStr(js, v, "x");
    JSValue y_val = JS_GetPropertyStr(js, v, "y");
    
    v2.X = js2number(js, x_val);
    v2.Y = js2number(js, y_val);
    
    JS_FreeValue(js, x_val);
    JS_FreeValue(js, y_val);
  }
  
  return v2;
}

HMM_Vec3 js2vec3(JSContext *js,JSValue v)
{
  HMM_Vec3 v3;
  v3.x = js_getnum_uint32(js, v,0);
  v3.y = js_getnum_uint32(js, v,1);
  v3.z = js_getnum_uint32(js, v,2);
  return v3;
}

float *js2floats(JSContext *js, JSValue v, size_t *len)
{
  *len = JS_ArrayLength(js,v);
  float *arr = malloc(sizeof(float)* *len);
  for (int i = 0; i < *len; i++)
    arr[i] = js_getnum_uint32(js,v,i);
  return arr;
}

double *js2doubles(JSContext *js, JSValue v, size_t *len)
{
  *len = JS_ArrayLength(js,v);
  double *arr = malloc(sizeof(double)* *len);
  for (int i = 0; i < *len; i++)
    arr[i] = js_getnum_uint32(js,v,i);
  return arr;
}

HMM_Vec3 js2vec3f(JSContext *js, JSValue v)
{
  HMM_Vec3 vec;
  if (JS_IsArray(js, v))
    return js2vec3(js,v);
  else
    vec.x = vec.y = vec.z = js2number(js,v);
  return vec;
}

JSValue vec32js(JSContext *js, HMM_Vec3 v)
{
  JSValue array = JS_NewArray(js);
  JS_SetPropertyUint32(js, array,0,number2js(js,v.x));
  JS_SetPropertyUint32(js, array,1,number2js(js,v.y));
  JS_SetPropertyUint32(js, array,2,number2js(js,v.z));
  return array;
}

JSValue vec3f2js(JSContext *js, HMM_Vec3 v)
{
  return vec32js(js,v);
}

JSValue quat2js(JSContext *js, HMM_Quat q)
{
  JSValue arr = JS_NewArray(js);
  JS_SetPropertyUint32(js, arr, 0, number2js(js,q.x));
  JS_SetPropertyUint32(js, arr,1,number2js(js,q.y));
  JS_SetPropertyUint32(js, arr,2,number2js(js,q.z));
  JS_SetPropertyUint32(js, arr,3,number2js(js,q.w));
  return arr;
}

HMM_Vec4 js2vec4(JSContext *js, JSValue v)
{
  HMM_Vec4 v4;
  for (int i = 0; i < 4; i++)
    v4.e[i] = js_getnum_uint32(js, v,i);
  return v4;
}

double arr_vec_length(JSContext *js,JSValue v)
{
  int len = JS_ArrayLength(js,v);
  switch(len) {
    case 2: return HMM_LenV2(js2vec2(js,v));
    case 3: return HMM_LenV3(js2vec3(js,v));
    case 4: return HMM_LenV4(js2vec4(js,v));
  }

  double sum = 0;
  for (int i = 0; i < len; i++)
    sum += pow(js_getnum_uint32(js, v, i), 2);

  return sqrt(sum);
}

HMM_Quat js2quat(JSContext *js,JSValue v)
{
  return js2vec4(js,v).quat;
}

JSValue vec42js(JSContext *js, HMM_Vec4 v)
{
  JSValue array = JS_NewArray(js);
  for (int i = 0; i < 4; i++)
    JS_SetPropertyUint32(js, array,i,number2js(js,v.e[i]));
  return array;
}

HMM_Vec2 *js2cpvec2arr(JSContext *js,JSValue v) {
  HMM_Vec2 *arr = NULL;
  int n = JS_ArrayLength(js,v);
  arrsetlen(arr,n);
  
  for (int i = 0; i < n; i++)
    arr[i] = js2vec2(js,js_getpropertyuint32(js,  v, i));
    
  return arr;
}

JSValue vecarr2js(JSContext *js,HMM_Vec2 *points, int n) {
  JSValue array = JS_NewArray(js);
  for (int i = 0; i < n; i++)
    JS_SetPropertyUint32(js, array,i,vec22js(js,points[i]));
    
  return array;
}

rect js2rect(JSContext *js,JSValue v) {
  if (JS_IsNull(v)) return (rect){0,0,1,1};
  rect rect;
  JS_GETATOM(js,rect.x,v,x,number)
  JS_GETATOM(js,rect.y,v,y,number)
  JS_GETATOM(js,rect.w,v,width,number)
  JS_GETATOM(js,rect.h,v,height,number)
  float anchor_x, anchor_y;
  JS_GETATOM(js, anchor_x, v, anchor_x, number)
  JS_GETATOM(js, anchor_y, v, anchor_y, number)

  rect.y -= anchor_y*rect.h;
  rect.x -= anchor_x*rect.w;

  return rect;
}

irect js2irect(JSContext *js, JSValue v)
{
  if (JS_IsNull(v)) return (irect){0,0,1,1};
  irect rect;
  JS_GETATOM(js,rect.x,v,x,number)
  JS_GETATOM(js,rect.y,v,y,number)
  JS_GETATOM(js,rect.w,v,width,number)
  JS_GETATOM(js,rect.h,v,height,number)
  float anchor_x, anchor_y;
  JS_GETATOM(js, anchor_x, v, anchor_x, number)
  JS_GETATOM(js, anchor_y, v, anchor_y, number)

  rect.y -= anchor_y*rect.h;
  rect.x -= anchor_x*rect.w;

  return rect;
}

JSValue rect2js(JSContext *js,rect rect) {
  JSValue obj = JS_NewObject(js);
  JS_SetPropertyStr(js, obj, "x", number2js(js, rect.x));
  JS_SetPropertyStr(js, obj, "y", number2js(js, rect.y));
  JS_SetPropertyStr(js, obj, "width", number2js(js, rect.w));
  JS_SetPropertyStr(js, obj, "height", number2js(js, rect.h));
  return obj;
}

static JSValue floats2array(JSContext *js, float *vals, size_t len) {
  JSValue arr = JS_NewArray(js);
  for (size_t i = 0; i < len; i++) {
    JS_SetPropertyUint32(js, arr, i, number2js(js, vals[i]));
  }
  return arr;
}

#define JS_HMM_FN(OP, HMM, SIGN) \
JSC_CCALL(array_##OP, \
  int len = JS_ArrayLength(js,self); \
  if (!JS_IsArray(js, argv[0])) { \
    double n = js2number(js,argv[0]); \
    JSValue arr = JS_NewArray(js); \
    for (int i = 0; i < len; i++) \
      JS_SetPropertyUint32(js, arr, i, number2js(js,js_getnum_uint32(js, self,i) SIGN n)); \
    return arr; \
  } \
  switch(len) { \
    case 2: \
      return vec22js(js,HMM_##HMM##V2(js2vec2(js,self), js2vec2(js,argv[0]))); \
    case 3: \
      return vec32js(js, HMM_##HMM##V3(js2vec3(js,self), js2vec3(js,argv[0]))); \
    case 4: \
      return vec42js(js,HMM_##HMM##V4(js2vec4(js,self), js2vec4(js,argv[0]))); \
  } \
  \
  JSValue arr = JS_NewArray(js); \
  for (int i = 0; i < len; i++) { \
    double a = js_getnum_uint32(js, self,i); \
    double b = js_getnum_uint32(js, argv[0],i); \
    JS_SetPropertyUint32(js, arr, i, number2js(js,a SIGN b)); \
  } \
  return arr; \
) \

JS_HMM_FN(add, Add, +)
JS_HMM_FN(sub, Sub, -)
JS_HMM_FN(div, Div, /)
JS_HMM_FN(scale, Mul, *)

JSC_CCALL(array_lerp,
  double t = js2number(js,argv[1]);
  int len = JS_ArrayLength(js,self);
  JSValue arr =  JS_NewArray(js);
  
  for (int i = 0; i < len; i++) {
    double from = js_getnum_uint32(js, self, i);
    double to = js_getnum_uint32(js, argv[0], i);
    JS_SetPropertyUint32(js, arr, i, number2js(js,(to - from) * t + from));
  }
  return arr;
)

JSValue js_array_get_x(JSContext *js, JSValue self)  { return JS_GetPropertyUint32(js,self,0); }
JSValue js_array_set_x(JSContext *js, JSValue self, JSValue val) { JS_SetPropertyUint32(js,self,0,val); return JS_NULL; }

JSValue js_array_get_y(JSContext *js, JSValue self) { return JS_GetPropertyUint32(js,self,1); }
JSValue js_array_set_y(JSContext *js, JSValue self, JSValue val) { JS_SetPropertyUint32(js,self,1,val); return JS_NULL; }

// Single-value accessors

JSValue js_array_get_r(JSContext *js, JSValue self)
{ return JS_GetPropertyUint32(js, self, 0); }

JSValue js_array_set_r(JSContext *js, JSValue self, JSValue val)
{ JS_SetPropertyUint32(js, self, 0, val); return JS_NULL; }

JSValue js_array_get_g(JSContext *js, JSValue self)
{ return JS_GetPropertyUint32(js, self, 1); }

JSValue js_array_set_g(JSContext *js, JSValue self, JSValue val)
{ JS_SetPropertyUint32(js, self, 1, val); return JS_NULL; }

JSValue js_array_get_b(JSContext *js, JSValue self)
{ return JS_GetPropertyUint32(js, self, 2); }

JSValue js_array_set_b(JSContext *js, JSValue self, JSValue val)
{ JS_SetPropertyUint32(js, self, 2, val); return JS_NULL; }

JSValue js_array_get_a(JSContext *js, JSValue self)
{ return JS_GetPropertyUint32(js, self, 3); }

JSValue js_array_set_a(JSContext *js, JSValue self, JSValue val)
{ JS_SetPropertyUint32(js, self, 3, val); return JS_NULL; }

// Multi-value accessors
static const JSCFunctionListEntry js_array_funcs[] = {
  PROTO_FUNC_DEF(array, add, 1),
  PROTO_FUNC_DEF(array, sub, 1),
  PROTO_FUNC_DEF(array, div,1),
  PROTO_FUNC_DEF(array, scale, 1),
  PROTO_FUNC_DEF(array, lerp, 2),
  JS_CGETSET_DEF("x", js_array_get_x,js_array_set_x),
  JS_CGETSET_DEF("y", js_array_get_y, js_array_set_y),
  JS_CGETSET_DEF("r", js_array_get_r, js_array_set_r),
  JS_CGETSET_DEF("g", js_array_get_g, js_array_set_g),
  JS_CGETSET_DEF("b", js_array_get_b, js_array_set_b),
  JS_CGETSET_DEF("a", js_array_get_a, js_array_set_a),
};

JSC_CCALL(number_lerp,
  double a = js2number(js,self);
  double b = js2number(js,argv[0]);
  double t = js2number(js,argv[1]);
  return number2js(js, (b-a)*t+a);
)

static const JSCFunctionListEntry js_number_funcs[] = {
  PROTO_FUNC_DEF(number, lerp, 2),
};

static uint32_t rng_state = 123456789;
static uint32_t xorshift32(){
  uint32_t x = rng_state;
  x ^= x << 13;
  x ^= x >> 17;
  x ^= x << 5;
  return rng_state = x;
}

JSC_CCALL(os_guid,
  uint8_t data[16];
  for(int i = 0; i < 4; i++){
    uint32_t v = xorshift32();
    memcpy(&data[i*4], &v, 4);
  }
  
  static const char hex[] = "0123456789abcdef";
  char buf[32];
  for(int i = 0; i < 16; i++){
    uint8_t b = data[i];
    buf[i*2    ] = hex[b >> 4];
    buf[i*2 + 1] = hex[b & 0x0f];
  }

  return JS_NewStringLen(js, buf, 32);
)

JSC_SCALL(console_print, printf("%s", str); )

static const JSCFunctionListEntry js_console_funcs[] = {
  MIST_FUNC_DEF(console,print,1),
};

JSC_CCALL(datastream_time, return number2js(js,plm_get_time(js2datastream(js,self)->plm)); )
JSC_CCALL(datastream_seek, ds_seek(js2datastream(js,self), js2number(js,argv[0])))
JSC_CCALL(datastream_advance, ds_advance(js2datastream(js,self), js2number(js,argv[0])))
JSC_CCALL(datastream_duration, return number2js(js,ds_length(js2datastream(js,self))))
JSC_CCALL(datastream_framerate, return number2js(js,plm_get_framerate(js2datastream(js,self)->plm)))

JSC_GETSET_CALLBACK(datastream, callback)

static const JSCFunctionListEntry js_datastream_funcs[] = {
  MIST_FUNC_DEF(datastream, time, 0),
  MIST_FUNC_DEF(datastream, seek, 1),
  MIST_FUNC_DEF(datastream, advance, 1),
  MIST_FUNC_DEF(datastream, duration, 0),
  MIST_FUNC_DEF(datastream, framerate, 0),
  CGETSET_ADD(datastream, callback),
};

// input: (encoded image data of jpg, png, bmp, tiff)
JSC_CCALL(os_image_decode,
  size_t len;
  void *raw = js_get_blob_data(js, &len, argv[0]);
  if (!raw) return JS_ThrowReferenceError(js, "could not load texture with array buffer");

  int n, width, height;
  void *data = stbi_load_from_memory(raw, len, &width, &height, &n, 4);

  if (!data)
    return JS_ThrowReferenceError(js, "no known image type from pixel data: %s", stbi_failure_reason());
    
  if (width <= 0 || height <= 0) {
    free(data);
    return JS_ThrowReferenceError(js, "decoded image has invalid size: %dx%d", width, height);
  }
  
  int pitch = width*4;
  size_t pixels_size = pitch * height;
  
  // Create JS object with surface data
  JSValue obj = JS_NewObject(js);
  JS_SetPropertyStr(js, obj, "width", JS_NewInt32(js, width));
  JS_SetPropertyStr(js, obj, "height", JS_NewInt32(js, height));
  JS_SetPropertyStr(js, obj, "format", JS_NewString(js, "rgba32"));
  JS_SetPropertyStr(js, obj, "pitch", JS_NewInt32(js, pitch));
  JS_SetPropertyStr(js, obj, "pixels", js_new_blob_stoned_copy(js, data, pixels_size));
  JS_SetPropertyStr(js, obj, "depth", JS_NewInt32(js, 8));
  JS_SetPropertyStr(js, obj, "hdr", JS_NewBool(js,0));
  
  free(data);
  ret = obj;
)

// input: (gif image data)
JSC_CCALL(os_make_gif,
  size_t rawlen;
  void *raw = js_get_blob_data(js, &rawlen, argv[0]);
  if (!raw) return JS_ThrowReferenceError(js, "could not load gif from supplied array buffer");

  int n;
  int frames;
  int *delays;
  int width;
  int height;
  void *pixels = stbi_load_gif_from_memory(raw, rawlen, &delays, &width, &height, &frames, &n, 4);

  if (!pixels) {
    return JS_ThrowReferenceError(js, "Failed to decode GIF: %s", stbi_failure_reason());
  }

  // Always return an array of surfaces, even for single frame
  JSValue surface_array = JS_NewArray(js);
  ret = surface_array;

  for (int i = 0; i < frames; i++) {
    // Create surface data object
    JSValue surfData = JS_NewObject(js);
    JS_SetPropertyStr(js, surfData, "width", JS_NewInt32(js, width));
    JS_SetPropertyStr(js, surfData, "height", JS_NewInt32(js, height));
    JS_SetPropertyStr(js, surfData, "format", JS_NewString(js, "rgba32"));
    JS_SetPropertyStr(js, surfData, "pitch", JS_NewInt32(js, width*4));
    
    void *frame_pixels = (unsigned char*)pixels+(width*height*4*i);
    JS_SetPropertyStr(js, surfData, "pixels", js_new_blob_stoned_copy(js, frame_pixels, width*height*4));
    
    // Add time property for animation frames
    if (frames > 1 && delays) {
      JS_SetPropertyStr(js, surfData, "time", number2js(js,(float)delays[i]/1000.0));
    }
    
    JS_SetPropertyUint32(js, surface_array, i, surfData);
  }

CLEANUP:
  if (delays) free(delays);
  if (pixels) free(pixels);
)

JSValue aseframe2js(JSContext *js, ase_frame_t aframe)
{
  JSValue frame = JS_NewObject(js);
  
  // Create surface data object instead of SDL_Surface
  JSValue surfData = JS_NewObject(js);
  JS_SetPropertyStr(js, surfData, "width", JS_NewInt32(js, aframe.ase->w));
  JS_SetPropertyStr(js, surfData, "height", JS_NewInt32(js, aframe.ase->h));
  JS_SetPropertyStr(js, surfData, "format", JS_NewString(js, "rgba32"));
  JS_SetPropertyStr(js, surfData, "pitch", JS_NewInt32(js, aframe.ase->w*4));
  JS_SetPropertyStr(js, surfData, "pixels", js_new_blob_stoned_copy(js, aframe.pixels, aframe.ase->w*aframe.ase->h*4));
  
  JS_SetPropertyStr(js, frame, "surface", surfData);
  JS_SetPropertyStr(js, frame, "time", number2js(js,(float)aframe.duration_milliseconds/1000.0));
  return frame;
}

// input: (aseprite data)
JSC_CCALL(os_make_aseprite,
  size_t rawlen;
  void *raw = js_get_blob_data(js,&rawlen,argv[0]);
  
  ase_t *ase = cute_aseprite_load_from_memory(raw, rawlen, NULL);

  if (!ase)
    return JS_ThrowReferenceError(js, "could not load aseprite from supplied array buffer: %s", aseprite_GetError());

  if (ase->tag_count == 0) {
    // we're dealing with a single frame image, or single animation
    if (ase->frame_count == 1) {
      JSValue obj = aseframe2js(js,ase->frames[0]);
      cute_aseprite_free(ase);
      return obj;
    } else {
      // Multiple frames but no tags - create a simple animation
      JSValue obj = JS_NewObject(js);
      JSValue frames = JS_NewArray(js);
      for (int f = 0; f < ase->frame_count; f++) {
        JSValue frame = aseframe2js(js,ase->frames[f]);
        JS_SetPropertyUint32(js, frames, f, frame);
      }
      JS_SetPropertyStr(js, obj, "frames", frames);
      JS_SetPropertyStr(js, obj, "loop", JS_NewBool(js, true));
      ret = obj;
      cute_aseprite_free(ase);
      return ret;
    }
  }

  JSValue obj = JS_NewObject(js);

  for (int t = 0; t < ase->tag_count; t++) {
    ase_tag_t tag = ase->tags[t];
    JSValue anim = JS_NewObject(js);
    JS_SetPropertyStr(js, anim, "repeat", number2js(js,tag.repeat));
    switch(tag.loop_animation_direction) {
      case ASE_ANIMATION_DIRECTION_FORWARDS:
        JS_SetPropertyStr(js, anim, "loop", JS_NewString(js,"forward"));
        break;
      case ASE_ANIMATION_DIRECTION_BACKWORDS:
        JS_SetPropertyStr(js, anim, "loop", JS_NewString(js,"backward"));
        break;
      case ASE_ANIMATION_DIRECTION_PINGPONG:
        JS_SetPropertyStr(js, anim, "loop", JS_NewString(js,"pingpong"));
        break;
    }

    int _frame = 0;
    JSValue frames = JS_NewArray(js);
    for (int f = tag.from_frame; f <= tag.to_frame; f++) {
      JSValue frame = aseframe2js(js,ase->frames[f]);
      JS_SetPropertyUint32(js, frames, _frame, frame);
      _frame++;
    }
    JS_SetPropertyStr(js, anim, "frames", frames);
    JS_SetPropertyStr(js, obj, tag.name, anim);
  }

  ret = obj;

  cute_aseprite_free(ase);
)


JSC_SCALL(os_system, ret = number2js(js,system(str)); )

JSC_CCALL(os_rand,
  MTRand *mrand = &((cell_rt*)JS_GetContextOpaque(js))->mrand;
  return number2js(js, genRand(mrand));
)

JSC_CCALL(os_randi,
  MTRand *mrand = &((cell_rt*)JS_GetContextOpaque(js))->mrand;
  return JS_NewInt64(js, genRandLong(mrand));
)

JSC_CCALL(os_srand,
  MTRand *mrand = &((cell_rt*)JS_GetContextOpaque(js))->mrand;
  m_seedRand(mrand, js2number(js,argv[0]));
)

JSC_CCALL(os_make_line_prim,
  return JS_NULL;
  /*
  JSValue prim = JS_NewObject(js);
  HMM_Vec2 *v = js2cpvec2arr(js,argv[0]);
  
  parsl_context *par_ctx = parsl_create_context((parsl_config){
    .thickness = js2number(js,argv[1]),
    .flags= PARSL_FLAG_ANNOTATIONS,
    .u_mode = js2number(js,argv[2])
  });
  
  uint16_t spine_lens[] = {arrlen(v)};
  
  parsl_mesh *m = parsl_mesh_from_lines(par_ctx, (parsl_spine_list){
    .num_vertices = arrlen(v),
    .num_spines = 1,
    .vertices = v,
    .spine_lengths = spine_lens,
    .closed = JS_ToBool(js,argv[3])
  });

  JS_SetPropertyStr(js, prim, "pos", make_gpu_buffer(js,m->positions,sizeof(*m->positions)*m->num_vertices, 0, 2,1,0));
  
  JS_SetPropertyStr(js, prim, "indices", make_gpu_buffer(js,m->triangle_indices,sizeof(*m->triangle_indices)*m->num_triangles*3, JS_TYPED_ARRAY_UINT32, 1,1,1));
  
  float uv[m->num_vertices*2];
  for (int i = 0; i < m->num_vertices; i++) {
    uv[i*2] = m->annotations[i].u_along_curve;
    uv[i*2+1] = m->annotations[i].v_across_curve;
  }

  JS_SetPropertyStr(js, prim, "uv", make_gpu_buffer(js, uv, sizeof(uv), 0,2,1,0));
  JS_SetPropertyStr(js,prim,"vertices", number2js(js,m->num_vertices));
  JS_SetPropertyStr(js,prim,"num_indices", number2js(js,m->num_triangles*3));
  JS_SetPropertyStr(js,prim,"first_index", number2js(js,0));

  parsl_destroy_context(par_ctx);
  
  return prim;
  */
)

static void render_frame(plm_t *mpeg, plm_frame_t *frame, datastream *ds) {
  if (JS_IsNull(ds->callback)) return;
  uint8_t *rgb = malloc(frame->height*frame->width*4);
  memset(rgb,255,frame->height*frame->width*4);
  plm_frame_to_rgba(frame, rgb, frame->width*4);
  
  // Create surface data object instead of SDL_Surface
  JSValue surfData = JS_NewObject(ds->js);
  JS_SetPropertyStr(ds->js, surfData, "width", JS_NewInt32(ds->js, frame->width));
  JS_SetPropertyStr(ds->js, surfData, "height", JS_NewInt32(ds->js, frame->height));
  JS_SetPropertyStr(ds->js, surfData, "format", JS_NewString(ds->js, "rgba32"));
  JS_SetPropertyStr(ds->js, surfData, "pitch", JS_NewInt32(ds->js, frame->width*4));
  JS_SetPropertyStr(ds->js, surfData, "pixels", js_new_blob_stoned_copy(ds->js, rgb, frame->height*frame->width*4));
  
  JSValue s[1];
  s[0] = surfData;
  JSValue cb = JS_DupValue(ds->js,ds->callback);
  JSValue ret = JS_Call(ds->js, cb, JS_NULL, 1, s);
  JS_FreeValue(ds->js,cb);
  free(rgb);
  uncaught_exception(ds->js,ret);
}

JSC_CCALL(os_make_video,
  size_t len;
  void *data = js_get_blob_data(js,&len,argv[0]);
  datastream *ds = ds_openvideo(data, len);
  if (!ds) return JS_ThrowReferenceError(js, "Video file was not valid.");
  ds->js = js;
  ds->callback = JS_NULL;
  plm_set_video_decode_callback(ds->plm, render_frame, ds);
  return datastream2js(js,ds);  
)

JSC_CCALL(os_rectpack,
  int width = js2number(js,argv[0]);
  int height = js2number(js,argv[1]);
  int num = JS_ArrayLength(js,argv[2]);
  stbrp_context ctx[1];
  stbrp_rect rects[num];
  
  for (int i = 0; i < num; i++) {
    HMM_Vec2 wh = js2vec2(js,js_getpropertyuint32(js, argv[2], i));
    rects[i].w = wh.x;
    rects[i].h = wh.y;
    rects[i].id = i;
  }
  
  stbrp_node nodes[width];
  stbrp_init_target(ctx, width, height, nodes, width);
  int packed = stbrp_pack_rects(ctx, rects, num);
  
  if (!packed) {
    return JS_NULL;
  }
  
  ret = JS_NewArray(js);
  for (int i = 0; i < num; i++) {
    HMM_Vec2 pos;
    pos.x = rects[i].x;
    pos.y = rects[i].y;
    JS_SetPropertyUint32(js, ret, i, vec22js(js,pos));
  }
)

static const JSCFunctionListEntry js_util_funcs[] = {
  MIST_FUNC_DEF(os, guid, 0),
};

JSC_CCALL(graphics_hsl_to_rgb,
  float h, s, l;
  JS_ToFloat64(js, &h, argv[0]);
  JS_ToFloat64(js, &s, argv[1]);
  JS_ToFloat64(js, &l, argv[2]);
  float c = (1 - abs(2 * l - 1)) * s;
  float x = c * (1 - abs(fmod((h/60),2) - 1));
  float m = l - c / 2;
  float r = 0, g = 0, b = 0;
  
  if (h < 60) { r = c; g = x; }
  else if (h < 120) { r = x; g = c; }
  else if (h < 180) { g = c; b = x; }
  else if (h < 240) { g = x; b = c; }
  else if (h < 300) { r = x; b = c; }
  else { r = c; b = x; }
  
  return color2js(js, (colorf){r+m, g+m, b+m, 1});
)

static const JSCFunctionListEntry js_graphics_funcs[] = {
  MIST_FUNC_DEF(os, rectpack, 3),
  MIST_FUNC_DEF(os, image_decode, 1),
  MIST_FUNC_DEF(os, make_gif, 1),
  MIST_FUNC_DEF(os, make_aseprite, 1),
  MIST_FUNC_DEF(os, make_line_prim, 5),
  MIST_FUNC_DEF(graphics, hsl_to_rgb, 3),
};

static const JSCFunctionListEntry js_video_funcs[] = {
  MIST_FUNC_DEF(os, make_video, 1),
};

JSC_SCALL(os_use_embed,
  cell_rt *rt = JS_GetContextOpaque(js);
  ModuleEntry *module_registry = rt->module_registry;
  for (int i = 0; i < arrlen(module_registry); i++) {
    if (strcmp(str,module_registry[i].name) == 0) {
      ret = module_registry[i].fn(js);
      break;
    }
  }
)

JSC_SCALL(os_use_dyn,
  SDL_SharedObject *ptr = SDL_LoadObject(str);
  if (!ptr)
    return JS_ThrowReferenceError(js, "Shared library %s could not be loaded", SDL_GetError());
  
  JSValue (*js_use)(JSContext*);
  js_use = (JSValue (*)(JSContext*))SDL_LoadFunction(ptr, "use");

  if (!js_use)
    ret = JS_ThrowReferenceError(js, "Shared library %s has no use function", str);
  else
    ret = js_use(js);

  SDL_UnloadObject(ptr);
)

#define JSSTATIC(NAME, PARENT) \
js_##NAME = JS_NewObject(js); \
JS_SetPropertyFunctionList(js, js_##NAME, js_##NAME##_funcs, countof(js_##NAME##_funcs)); \
JS_SetPrototype(js, js_##NAME, PARENT); \

JSValue js_miniz_use(JSContext *js);
JSValue js_num_use(JSContext *js);

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

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

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

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

JSC_CCALL(os_value_id,
  JS_SetPropertyStr(js, argv[0], "id", number2js(js, (uintptr_t)JS_VALUE_GET_PTR(argv[0])));
  return argv[0];
)

#include "qjs_crypto.h"
#include "qjs_time.h"
#include "qjs_http.h"
#include "qjs_wota.h"
#include "qjs_socket.h"
#include "qjs_nota.h"
#include "qjs_layout.h"
#include "qjs_sdl_gpu.h"
#include "cell_qoi.h"

JSValue js_imgui_use(JSContext *js);
#define MISTLINE(NAME) (ModuleEntry){#NAME, js_##NAME##_use}

void ffi_load(JSContext *js)
{
  cell_rt *rt = JS_GetContextOpaque(js);
  
  JS_FreeValue(js, js_blob_use(js)); // juice blob
  
  uint64_t rr;
  randombytes(&rr,4);
  m_seedRand(&rt->mrand, rr);
  
  // cell modules
  arrput(rt->module_registry, MISTLINE(time));
  arrput(rt->module_registry, ((ModuleEntry){"math", js_math_use}));
  arrput(rt->module_registry, MISTLINE(blob));
  
  // extra
  arrput(rt->module_registry, ((ModuleEntry){"io", js_io_use}));
  arrput(rt->module_registry, ((ModuleEntry){"fd", js_fd_use}));
  arrput(rt->module_registry, MISTLINE(socket));
  arrput(rt->module_registry, ((ModuleEntry){"os", js_os_use}));
#ifdef HAVE_QRENCODE
  arrput(rt->module_registry, MISTLINE(qr));
#endif
  arrput(rt->module_registry, MISTLINE(http));
  arrput(rt->module_registry, MISTLINE(crypto));
  arrput(rt->module_registry, MISTLINE(miniz));
//  arrput(rt->module_registry, MISTLINE(num));
  arrput(rt->module_registry, MISTLINE(kim));
  arrput(rt->module_registry, MISTLINE(utf8));
  arrput(rt->module_registry, MISTLINE(fit));
  arrput(rt->module_registry, MISTLINE(text));
  arrput(rt->module_registry, MISTLINE(staef));
  arrput(rt->module_registry, MISTLINE(wota));
  arrput(rt->module_registry, MISTLINE(nota));
  arrput(rt->module_registry, MISTLINE(sdl_gpu));
  arrput(rt->module_registry, MISTLINE(qoi));
  
  // power user
  arrput(rt->module_registry, MISTLINE(js));
  arrput(rt->module_registry, MISTLINE(debug));
  
#ifndef __EMSCRIPTEN__
  arrput(rt->module_registry, MISTLINE(dmon));
#endif

  arrput(rt->module_registry, MISTLINE(util));

  // prosperon
  arrput(rt->module_registry, ((ModuleEntry){"sdl_audio", js_sdl_audio_use}));
  arrput(rt->module_registry, ((ModuleEntry){"sdl_video", js_sdl_video_use}));
  arrput(rt->module_registry, ((ModuleEntry){"input", js_input_use}));  
  arrput(rt->module_registry, ((ModuleEntry){"surface", js_sdl_surface_use}));
  arrput(rt->module_registry, MISTLINE(spline));
  arrput(rt->module_registry, ((ModuleEntry){"geometry", js_geometry_use}));
  arrput(rt->module_registry, MISTLINE(graphics));
  arrput(rt->module_registry, MISTLINE(video));
  arrput(rt->module_registry, MISTLINE(soloud));
  arrput(rt->module_registry, MISTLINE(layout));

//  arrput(rt->module_registry, MISTLINE(imgui));
  arrput(rt->module_registry, ((ModuleEntry){"camera", js_camera_use}));

  arrput(rt->module_registry, MISTLINE(rtree));
  arrput(rt->module_registry, MISTLINE(sprite));
  arrput(rt->module_registry, MISTLINE(transform));
  
  arrput(rt->module_registry, MISTLINE(imgui));
  
#ifndef NSTEAM
  arrput(rt->module_registry, MISTLINE(steam));
#endif

  JSValue globalThis = JS_GetGlobalObject(js);

  JSValue prosp = JS_NewObject(js);
  JS_SetPropertyStr(js,globalThis,"prosperon", prosp);

  JSValue jsarray = JS_GetPropertyStr(js,globalThis, "Array");
  JSValue array_proto = JS_GetPropertyStr(js,jsarray, "prototype");
  JS_SetPropertyFunctionList(js, array_proto, js_array_funcs, countof(js_array_funcs));
  JS_FreeValue(js,jsarray);
  JS_FreeValue(js,array_proto);

  JSValue jsnumber = JS_GetPropertyStr(js,globalThis, "Number");
  JSValue number_proto = JS_GetPropertyStr(js,jsnumber, "prototype");
  JS_SetPropertyFunctionList(js, number_proto, js_number_funcs, countof(js_number_funcs));
  JS_FreeValue(js,jsnumber);
  JS_FreeValue(js,number_proto);
  
  JSValue hidden_fn = JS_NewObject(js);
  // add engine.js-only functions to hidden_fn. It should grab them and then remove so nothing else can use them.
  
  // Add modules that should only be accessible to engine.js
  JS_SetPropertyStr(js, hidden_fn, "actor", js_actor_use(js));
  JS_SetPropertyStr(js, hidden_fn, "wota", js_wota_use(js));
  JS_SetPropertyStr(js, hidden_fn, "console", js_console_use(js));
  JS_SetPropertyStr(js, hidden_fn, "nota", js_nota_use(js));
  
#ifndef __EMSCRIPTEN__
  JS_SetPropertyStr(js, hidden_fn, "enet", js_enet_use(js));
#endif
  
  // Add functions that should only be accessible to engine.js
  JS_SetPropertyStr(js, hidden_fn, "use_dyn", JS_NewCFunction(js, js_os_use_dyn, "use_dyn", 1));
  JS_SetPropertyStr(js, hidden_fn, "use_embed", JS_NewCFunction(js, js_os_use_embed, "use_embed", 1));
  JS_SetPropertyStr(js, hidden_fn, "rand", JS_NewCFunction(js, js_os_rand, "rand", 0));
  JS_SetPropertyStr(js, hidden_fn, "randi", JS_NewCFunction(js, js_os_randi, "randi", 0));
  JS_SetPropertyStr(js, hidden_fn, "srand", JS_NewCFunction(js, js_os_srand, "srand", 1));
  
  const char actorsym_script[] = "var sym = Symbol(`actordata`); sym;";
  
  JSValue actorsym = JS_Eval(js, actorsym_script, sizeof(actorsym_script)-1, "internal", 0);
  
  JS_SetPropertyStr(js, hidden_fn, "actorsym", actorsym);
  
  rt->actor_sym = JS_ValueToAtom(js, actorsym);
  
  if (rt->init_wota) {
    JS_SetPropertyStr(js, hidden_fn, "init", wota2value(js, rt->init_wota));
    // init wota can now be freed
    free(rt->init_wota);
    rt->init_wota = NULL;
  }

  JS_SetPropertyStr(js, prosp, "hidden", hidden_fn);
  
  JS_FreeValue(js,globalThis);
}
