#include "qjs_sdl.h"
#include "jsffi.h"
#include "qjs_macros.h"
#include "cell.h"
#include "qjs_blob.h"
#include "stb_ds.h"
#include "qjs_actor.h"
#include "qjs_sdl_surface.h"

#include <SDL3/SDL.h>

// SDL Free functions
void SDL_Camera_free(JSRuntime *rt, SDL_Camera *cam)
{
  SDL_CloseCamera(cam);
}

void SDL_AudioStream_free(JSRuntime *rt, SDL_AudioStream *st) {
  SDL_DestroyAudioStream(st);
}

// Class definitions for SDL types
QJSCLASS(SDL_Camera,)
QJSCLASS(SDL_AudioStream,)


// CAMERA FUNCTIONS

// Pixel format enum conversion using the new system
ENUM_MAPPING_TABLE(SDL_PixelFormat) = {
  {SDL_PIXELFORMAT_UNKNOWN, "unknown"},
  {SDL_PIXELFORMAT_INDEX1LSB, "index1lsb"},
  {SDL_PIXELFORMAT_INDEX1MSB, "index1msb"},
  {SDL_PIXELFORMAT_INDEX2LSB, "index2lsb"},
  {SDL_PIXELFORMAT_INDEX2MSB, "index2msb"},
  {SDL_PIXELFORMAT_INDEX4LSB, "index4lsb"},
  {SDL_PIXELFORMAT_INDEX4MSB, "index4msb"},
  {SDL_PIXELFORMAT_INDEX8, "index8"},
  {SDL_PIXELFORMAT_RGB332, "rgb332"},
  {SDL_PIXELFORMAT_XRGB4444, "xrgb4444"},
  {SDL_PIXELFORMAT_XBGR4444, "xbgr4444"},
  {SDL_PIXELFORMAT_XRGB1555, "xrgb1555"},
  {SDL_PIXELFORMAT_XBGR1555, "xbgr1555"},
  {SDL_PIXELFORMAT_ARGB4444, "argb4444"},
  {SDL_PIXELFORMAT_RGBA4444, "rgba4444"},
  {SDL_PIXELFORMAT_ABGR4444, "abgr4444"},
  {SDL_PIXELFORMAT_BGRA4444, "bgra4444"},
  {SDL_PIXELFORMAT_ARGB1555, "argb1555"},
  {SDL_PIXELFORMAT_RGBA5551, "rgba5551"},
  {SDL_PIXELFORMAT_ABGR1555, "abgr1555"},
  {SDL_PIXELFORMAT_BGRA5551, "bgra5551"},
  {SDL_PIXELFORMAT_RGB565, "rgb565"},
  {SDL_PIXELFORMAT_BGR565, "bgr565"},
  {SDL_PIXELFORMAT_RGB24, "rgb24"},
  {SDL_PIXELFORMAT_BGR24, "bgr24"},
  {SDL_PIXELFORMAT_XRGB8888, "xrgb8888"},
  {SDL_PIXELFORMAT_RGBX8888, "rgbx8888"},
  {SDL_PIXELFORMAT_XBGR8888, "xbgr8888"},
  {SDL_PIXELFORMAT_BGRX8888, "bgrx8888"},
  {SDL_PIXELFORMAT_ARGB8888, "argb8888"},
  {SDL_PIXELFORMAT_RGBA8888, "rgba8888"},
  {SDL_PIXELFORMAT_ABGR8888, "abgr8888"},
  {SDL_PIXELFORMAT_BGRA8888, "bgra8888"},
  {SDL_PIXELFORMAT_XRGB2101010, "xrgb2101010"},
  {SDL_PIXELFORMAT_XBGR2101010, "xbgr2101010"},
  {SDL_PIXELFORMAT_ARGB2101010, "argb2101010"},
  {SDL_PIXELFORMAT_ABGR2101010, "abgr2101010"},
  {SDL_PIXELFORMAT_RGB48, "rgb48"},
  {SDL_PIXELFORMAT_BGR48, "bgr48"},
  {SDL_PIXELFORMAT_RGBA64, "rgba64"},
  {SDL_PIXELFORMAT_ARGB64, "argb64"},
  {SDL_PIXELFORMAT_BGRA64, "bgra64"},
  {SDL_PIXELFORMAT_ABGR64, "abgr64"},
  {SDL_PIXELFORMAT_RGB48_FLOAT, "rgb48_float"},
  {SDL_PIXELFORMAT_BGR48_FLOAT, "bgr48_float"},
  {SDL_PIXELFORMAT_RGBA64_FLOAT, "rgba64_float"},
  {SDL_PIXELFORMAT_ARGB64_FLOAT, "argb64_float"},
  {SDL_PIXELFORMAT_BGRA64_FLOAT, "bgra64_float"},
  {SDL_PIXELFORMAT_ABGR64_FLOAT, "abgr64_float"},
  {SDL_PIXELFORMAT_RGB96_FLOAT, "rgb96_float"},
  {SDL_PIXELFORMAT_BGR96_FLOAT, "bgr96_float"},
  {SDL_PIXELFORMAT_RGBA128_FLOAT, "rgba128_float"},
  {SDL_PIXELFORMAT_ARGB128_FLOAT, "argb128_float"},
  {SDL_PIXELFORMAT_BGRA128_FLOAT, "bgra128_float"},
  {SDL_PIXELFORMAT_ABGR128_FLOAT, "abgr128_float"},
  {SDL_PIXELFORMAT_YV12, "yv12"},
  {SDL_PIXELFORMAT_IYUV, "iyuv"},
  {SDL_PIXELFORMAT_YUY2, "yuy2"},
  {SDL_PIXELFORMAT_UYVY, "uyvy"},
  {SDL_PIXELFORMAT_YVYU, "yvyu"},
  {SDL_PIXELFORMAT_NV12, "nv12"},
  {SDL_PIXELFORMAT_NV21, "nv21"},
  {SDL_PIXELFORMAT_P010, "p010"},
  {SDL_PIXELFORMAT_RGBA32, "rgba32"}
};
JS2ENUM(SDL_PixelFormat)

static JSValue cameraspec2js(JSContext *js, const SDL_CameraSpec *spec) {
  JSValue obj = JS_NewObject(js);
  
  JS_SetPropertyStr(js, obj, "format", SDL_PixelFormat2js(js, spec->format));
  JS_SetPropertyStr(js, obj, "colorspace", JS_NewInt32(js, spec->colorspace));
  JS_SetPropertyStr(js, obj, "width", JS_NewInt32(js, spec->width));
  JS_SetPropertyStr(js, obj, "height", JS_NewInt32(js, spec->height));
  JS_SetPropertyStr(js, obj, "framerate_numerator", JS_NewInt32(js, spec->framerate_numerator));
  JS_SetPropertyStr(js, obj, "framerate_denominator", JS_NewInt32(js, spec->framerate_denominator));
  
  return obj;
}

static SDL_CameraSpec js2cameraspec(JSContext *js, JSValue obj) {
  SDL_CameraSpec spec = {0};
  
  JSValue v;
  
  v = JS_GetPropertyStr(js, obj, "format");
  if (!JS_IsNull(v)) {
    spec.format = js2SDL_PixelFormat(js, v);
  }
  JS_FreeValue(js, v);
  
  v = JS_GetPropertyStr(js, obj, "colorspace");
  if (!JS_IsNull(v)) JS_ToInt32(js, &spec.colorspace, v);
  JS_FreeValue(js, v);
  
  v = JS_GetPropertyStr(js, obj, "width");
  if (!JS_IsNull(v)) JS_ToInt32(js, &spec.width, v);
  JS_FreeValue(js, v);
  
  v = JS_GetPropertyStr(js, obj, "height");
  if (!JS_IsNull(v)) JS_ToInt32(js, &spec.height, v);
  JS_FreeValue(js, v);
  
  v = JS_GetPropertyStr(js, obj, "framerate_numerator");
  if (!JS_IsNull(v)) JS_ToInt32(js, &spec.framerate_numerator, v);
  JS_FreeValue(js, v);
  
  v = JS_GetPropertyStr(js, obj, "framerate_denominator");
  if (!JS_IsNull(v)) JS_ToInt32(js, &spec.framerate_denominator, v);
  JS_FreeValue(js, v);
  
  return spec;
}

JSC_CCALL(camera_list,
  int num;
  JSValue jsids = JS_NewArray(js);  
  SDL_CameraID *ids = SDL_GetCameras(&num);
  for (int i = 0; i < num; i++)
    JS_SetPropertyUint32(js,jsids, i, number2js(js,ids[i]));

  return jsids;
)

JSC_CCALL(camera_open,
  int id = js2number(js,argv[0]);
  SDL_CameraSpec *spec_ptr = NULL;
  SDL_CameraSpec spec;
  
  // Check if a format spec was provided
  if (argc > 1 && !JS_IsNull(argv[1])) {
    spec = js2cameraspec(js, argv[1]);
    spec_ptr = &spec;
  }
  
  SDL_Camera *cam = SDL_OpenCamera(id, spec_ptr);
  if (!cam) ret = JS_ThrowReferenceError(js, "Could not open camera %d: %s\n", id, SDL_GetError());
  else
    ret = SDL_Camera2js(js,cam);
)

JSC_CCALL(camera_name,
  const char *name = SDL_GetCameraName(js2number(js,argv[0]));
  if (!name) return JS_ThrowReferenceError(js, "Could not get camera name from id %d.", (int)js2number(js,argv[0]));
  
  return JS_NewString(js, name);
)

JSC_CCALL(camera_position,
  SDL_CameraPosition pos = SDL_GetCameraPosition(js2number(js,argv[0]));
  switch(pos) {
    case SDL_CAMERA_POSITION_UNKNOWN: return JS_NewString(js,"unknown");
    case SDL_CAMERA_POSITION_FRONT_FACING: return JS_NewString(js,"front");
    case SDL_CAMERA_POSITION_BACK_FACING: return JS_NewString(js,"back");
  } 
)

JSC_CCALL(camera_drivers,
  int num = SDL_GetNumCameraDrivers();
  JSValue arr = JS_NewArray(js);
  for (int i = 0; i < num; i++)
    JS_SetPropertyUint32(js, arr, i, JS_NewString(js, SDL_GetCameraDriver(i)));
  return arr;
)

JSC_CCALL(camera_supported_formats,
  SDL_CameraID id = js2number(js,argv[0]);
  int num;
  SDL_CameraSpec **specs = SDL_GetCameraSupportedFormats(id, &num);
  
  if (!specs)
    return JS_ThrowReferenceError(js, "Could not get supported formats for camera %d: %s", id, SDL_GetError());
  
  JSValue arr = JS_NewArray(js);
  for (int i = 0; i < num; i++) {
    JS_SetPropertyUint32(js, arr, i, cameraspec2js(js, specs[i]));
  }
  
  SDL_free(specs);
  return arr;
)

static const JSCFunctionListEntry js_camera_funcs[] = {
  MIST_FUNC_DEF(camera, list, 0),
  MIST_FUNC_DEF(camera, open, 2),
  MIST_FUNC_DEF(camera, name, 1),
  MIST_FUNC_DEF(camera, position, 1),
  MIST_FUNC_DEF(camera, drivers, 0),
  MIST_FUNC_DEF(camera, supported_formats, 1),
};

JSC_CCALL(camera_capture,
  SDL_ClearError();
  SDL_Camera *cam = js2SDL_Camera(js,self);
  if (!cam) return JS_ThrowReferenceError(js,"Self was not a camera: %s", SDL_GetError());
  
  SDL_Surface *surf = SDL_AcquireCameraFrame(cam, NULL);
  if (!surf) {
    const char *msg = SDL_GetError();
    if (msg[0] != 0)
      return JS_ThrowReferenceError(js,"Could not get camera frame: %s", SDL_GetError());
    else return JS_NULL;
  }
  
  // Create a copy of the surface
  SDL_Surface *newsurf = SDL_CreateSurface(surf->w, surf->h, surf->format);
  
  if (!newsurf) {
    SDL_ReleaseCameraFrame(cam, surf);
    return JS_ThrowReferenceError(js, "Could not create surface: %s", SDL_GetError());
  }
  
  // Copy the surface data
  int result = SDL_BlitSurface(surf, NULL, newsurf, NULL);
  
  // Release the camera frame
  SDL_ReleaseCameraFrame(cam, surf);
  
  if (result != 0) {
    SDL_DestroySurface(newsurf);
    return JS_ThrowReferenceError(js, "Could not blit surface: %s", SDL_GetError());
  }

  return SDL_Surface2js(js,newsurf);
)

JSC_CCALL(camera_get_driver,
  SDL_Camera *cam = js2SDL_Camera(js,self);
  if (!cam) return JS_ThrowReferenceError(js,"Self was not a camera: %s", SDL_GetError());
  
  const char *driver = SDL_GetCurrentCameraDriver();
  if (!driver) return JS_NULL;
  
  return JS_NewString(js, driver);
)

JSC_CCALL(camera_get_format,
  SDL_Camera *cam = js2SDL_Camera(js,self);
  if (!cam) return JS_ThrowReferenceError(js,"Self was not a camera: %s", SDL_GetError());
  
  SDL_CameraSpec spec;
  if (!SDL_GetCameraFormat(cam, &spec))
    return JS_ThrowReferenceError(js, "Could not get camera format: %s", SDL_GetError());
  
  return cameraspec2js(js, &spec);
)

static const JSCFunctionListEntry js_SDL_Camera_funcs[] = 
{
  MIST_FUNC_DEF(camera, capture, 0),
  MIST_FUNC_DEF(camera, get_driver, 0),
  MIST_FUNC_DEF(camera, get_format, 0),
};

JSValue js_camera_use(JSContext *js) {
  SDL_Init(SDL_INIT_CAMERA);
  JSValue mod = JS_NewObject(js);
  JS_SetPropertyFunctionList(js,mod,js_camera_funcs,countof(js_camera_funcs));
  QJSCLASSPREP_FUNCS(SDL_Camera)
  return mod;
}

// SDL AUDIO FUNCTIONS

// Audio format lookup table and conversion functions
static const struct { const char *s; SDL_AudioFormat f; } fmt_lut[] = {
  { "u8",  SDL_AUDIO_U8  },   /* Unsigned 8-bit  */
  { "s8",  SDL_AUDIO_S8  },   /* Signed   8-bit  */
  { "s16", SDL_AUDIO_S16 },   /* Signed  16-bit, host endian */
  { "s32", SDL_AUDIO_S32 },   /* Signed  32-bit, host endian */
  { "f32", SDL_AUDIO_F32 }    /* Float   32-bit, host endian */
};

static int format_str_to_enum(const char *f, SDL_AudioFormat *out)
{
  struct { const char *s; SDL_AudioFormat f; } map[] = {
    {"u8",  SDL_AUDIO_U8 }, {"s16", SDL_AUDIO_S16},
    {"s32", SDL_AUDIO_S32}, {"f32", SDL_AUDIO_F32}
  };
  for (size_t i=0;i<countof(map);++i)
    if (!strcmp(f,map[i].s)) { *out = map[i].f; return 1; }
    
  return 0;
}

static const char *fmt2str(SDL_AudioFormat f)
{
  for (size_t i = 0; i < countof(fmt_lut); ++i)
    if (fmt_lut[i].f == f) return fmt_lut[i].s;
  return "unknown";
}

static JSValue audiospec2js(JSContext *js, const SDL_AudioSpec *spec)
{
  JSValue o = JS_NewObject(js);

  /* stringify format (u8/s16/s32/f32) */
  JS_SetPropertyStr(js, o, "format",
    JS_NewString(js, fmt2str(spec->format)));

  JS_SetPropertyStr(js, o, "channels",
    JS_NewInt32(js, spec->channels));

  JS_SetPropertyStr(js, o, "samplerate",
    JS_NewInt32(js, spec->freq));

  return o;
}

static SDL_AudioSpec js2audiospec(JSContext *js, JSValue obj)
{
  SDL_AudioSpec spec;

  JSValue v;

  v = JS_GetPropertyStr(js, obj, "format");
  if (!JS_IsNull(v)) {
    const char *s = JS_ToCString(js, v);
    format_str_to_enum(s, &spec.format);
    JS_FreeCString(js, s);
  }
  JS_FreeValue(js, v);

  v = JS_GetPropertyStr(js, obj, "channels");
  if (!JS_IsNull(v)) JS_ToInt32(js, &spec.channels, v);
  JS_FreeValue(js, v);

  v = JS_GetPropertyStr(js, obj, "samplerate");
  if (!JS_IsNull(v)) JS_ToInt32(js, &spec.freq, v);
  JS_FreeValue(js, v);

  return spec;
}

JSC_CCALL(sdl_audio_drivers,
  int num = SDL_GetNumAudioDrivers();
  JSValue arr = JS_NewArray(js);
  for (int i = 0; i < num; i++)
    JS_SetPropertyUint32(js, arr, i, JS_NewString(js, SDL_GetAudioDriver(i)));
  return arr;
)

JSC_CCALL(sdl_audio_devices,
  int n;
  SDL_AudioDeviceID *ids = SDL_GetAudioPlaybackDevices(&n);

  JSValue arr = JS_NewArray(js);
  for (int i = 0; i < n; i++)
    JS_SetPropertyUint32(js,arr,i,JS_NewString(js, SDL_GetAudioDeviceName(ids[i])));

  return arr;
)

JSC_CCALL(sdl_audio_open_stream,
  const char *type = JS_IsString(argv[0]) ? JS_ToCString(js, argv[0]) : NULL;
  SDL_AudioDeviceID devid = !strcmp(type, "capture") ? SDL_AUDIO_DEVICE_DEFAULT_RECORDING : SDL_AUDIO_DEVICE_DEFAULT_PLAYBACK;

  if (type)
    JS_FreeCString(js, type);

  SDL_AudioStream *st;
  
  if (JS_IsNull(argv[1]))
    st = SDL_OpenAudioDeviceStream(devid, NULL, NULL, NULL);
  else {
    SDL_AudioSpec want = js2audiospec(js, argv[1]);
    st = SDL_OpenAudioDeviceStream(devid, &want, NULL, NULL);
  }

  if (!st)
    return JS_ThrowInternalError(js, "open failed: %s", SDL_GetError());

  return SDL_AudioStream2js(js, st);
)

static const JSCFunctionListEntry js_sdl_audio_funcs[] = {
  MIST_FUNC_DEF(sdl_audio, drivers, 0),
  MIST_FUNC_DEF(sdl_audio, devices, 0),
  MIST_FUNC_DEF(sdl_audio, open_stream, 2),
};

JSC_CCALL(sdl_audiostream_get_format,
  SDL_AudioStream *as = js2SDL_AudioStream(js, self);
  SDL_AudioSpec src;
  SDL_AudioSpec dst;
  SDL_GetAudioStreamFormat(as, &src, &dst);
  JSValue obj = JS_NewObject(js);
  JS_SetPropertyStr(js, obj, "src", audiospec2js(js, &src));
  JS_SetPropertyStr(js, obj, "dst", audiospec2js(js, &dst));
  return obj;
)

JSC_CCALL(sdl_audiostream_set_format,
  SDL_AudioStream *as=js2SDL_AudioStream(js,self);
  const SDL_AudioSpec *src_ptr=NULL,*dst_ptr=NULL;
  SDL_AudioSpec src={0},dst={0};

  if(argc>0&&!JS_IsNull(argv[0])){
    src=js2audiospec(js,argv[0]);
    src_ptr=&src;
  }
  if(argc>1&&!JS_IsNull(argv[1])){
    dst=js2audiospec(js,argv[1]);
    dst_ptr=&dst;
  }

  if(!SDL_SetAudioStreamFormat(as,src_ptr,dst_ptr))
    return JS_ThrowInternalError(js,"%s",SDL_GetError());

  return JS_NULL;
)

JSC_CCALL(sdl_audiostream_resume,
  SDL_AudioStream *as = js2SDL_AudioStream(js,self);
  if (!SDL_ResumeAudioStreamDevice(as))
    return JS_ThrowInternalError(js,"%s",SDL_GetError());

  return JS_NULL;
)

JSC_CCALL(sdl_audiostream_clear,
  SDL_AudioStream *as=js2SDL_AudioStream(js,self);
  if (!SDL_ClearAudioStream(as))
    return JS_ThrowInternalError(js,"%s",SDL_GetError());
  return JS_NULL;
)

JSC_CCALL(sdl_audiostream_flush,
  SDL_AudioStream *as=js2SDL_AudioStream(js,self);
  if(!SDL_FlushAudioStream(as))
    return JS_ThrowInternalError(js,"%s",SDL_GetError());
  return JS_NULL;
)

JSC_CCALL(sdl_audiostream_available,
  SDL_AudioStream *as=js2SDL_AudioStream(js,self);
  Sint64 n = SDL_GetAudioStreamAvailable(as);
  if(n<0) return JS_ThrowInternalError(js,"%s",SDL_GetError());
  return JS_NewInt64(js,n);
)

JSC_CCALL(sdl_audiostream_queued,
  SDL_AudioStream *as=js2SDL_AudioStream(js,self);
  Sint64 n = SDL_GetAudioStreamQueued(as);
  if(n<0) return JS_ThrowInternalError(js,"%s",SDL_GetError());
  return JS_NewInt64(js,n);
)

/* ---------- data IO ---------------------------------------------------- */
JSC_CCALL(sdl_audiostream_put,
  SDL_AudioStream *as=js2SDL_AudioStream(js,self);
  size_t len;
  void *buf = js_get_blob_data(js, &len, argv[0]);
  if (!buf)
    return JS_ThrowInternalError(js, "Requires array buffer.");

  if (!SDL_PutAudioStreamData(as,buf,len))
    return JS_ThrowInternalError(js, "%s", SDL_GetError());
  return JS_NULL;
)

JSC_CCALL(sdl_audiostream_get,
  SDL_AudioStream *as=js2SDL_AudioStream(js,self);
  int want;
  JS_ToInt32(js,&want,argv[0]);
  void *data = malloc(want);
  int got = SDL_GetAudioStreamData(as, data, want);
  
  if (got<0) {
    free(data);
    return JS_ThrowInternalError(js,"%s",SDL_GetError());
  }
  
  JSValue ab = js_new_blob_stoned_copy(js, data, got);
  free(data);

  return ab;
)

JSC_CCALL(sdl_audiostream_get_gain,
  SDL_AudioStream *as=js2SDL_AudioStream(js,self);
  return JS_NewFloat64(js,SDL_GetAudioStreamGain(as));
)

JSC_CCALL(sdl_audiostream_set_gain,
  SDL_AudioStream *as=js2SDL_AudioStream(js,self);
  double g; JS_ToFloat64(js,&g,argv[0]);
  SDL_SetAudioStreamGain(as,(float)g);
  return JS_NULL;
)

JSC_CCALL(sdl_audiostream_get_freq_ratio,
  SDL_AudioStream *as=js2SDL_AudioStream(js,self);
  return JS_NewFloat64(js,SDL_GetAudioStreamFrequencyRatio(as));
)

JSC_CCALL(sdl_audiostream_set_freq_ratio,
  SDL_AudioStream *as=js2SDL_AudioStream(js,self);
  double r; JS_ToFloat64(js,&r,argv[0]);
  SDL_SetAudioStreamFrequencyRatio(as,(float)r);
  return JS_NULL;
)

/* ---------- JS export list -------------------------------------------- */
static const JSCFunctionListEntry js_SDL_AudioStream_funcs[] = {
  MIST_FUNC_DEF(sdl_audiostream, get_format,            0),
  MIST_FUNC_DEF(sdl_audiostream, set_format,            2),
  MIST_FUNC_DEF(sdl_audiostream, resume,                0),
  MIST_FUNC_DEF(sdl_audiostream, clear,                 0),
  MIST_FUNC_DEF(sdl_audiostream, flush,                 0),
  MIST_FUNC_DEF(sdl_audiostream, available,             0),
  MIST_FUNC_DEF(sdl_audiostream, queued,                0),
  MIST_FUNC_DEF(sdl_audiostream, put,                   1),
  MIST_FUNC_DEF(sdl_audiostream, get,                   1),
  MIST_FUNC_DEF(sdl_audiostream, set_gain,              1),
  MIST_FUNC_DEF(sdl_audiostream, get_gain,              0),
  MIST_FUNC_DEF(sdl_audiostream, set_freq_ratio,        1),
  MIST_FUNC_DEF(sdl_audiostream, get_freq_ratio,        0),
};

JSValue js_sdl_audio_use(JSContext *js) {
  if (!SDL_Init(SDL_INIT_AUDIO))
    return JS_ThrowInternalError(js, "Unable to initialize audio subsystem: %s", SDL_GetError());
    
  QJSCLASSPREP_FUNCS(SDL_AudioStream)
  
  JSValue mod = JS_NewObject(js);
  JS_SetPropertyFunctionList(js,mod,js_sdl_audio_funcs,countof(js_sdl_audio_funcs));
  return mod;
}
