#include "qjs_sdl_surface.h"
#include "qjs_blob.h"
#include "jsffi.h"
#include <SDL3/SDL.h>
#include <SDL3/SDL_gpu.h>
#include <SDL3/SDL_surface.h>
#include <string.h>
#include <stdint.h>
#include "qjs_sdl.h"
#include "qjs_common.h"

#define STB_DXT_IMPLEMENTATION
#include "thirdparty/stb/stb_dxt.h"

JSValue pixelformat2js(JSContext *js, SDL_PixelFormat fmt)
{
  return SDL_PixelFormat2js(js, fmt);
}

SDL_PixelFormat js2pixelformat(JSContext *js, JSValue v)
{
  return js2SDL_PixelFormat(js, v);
}   

typedef struct { const char *name; SDL_ScaleMode mode; } scale_entry;

static const scale_entry k_scale_table[] = {
  { "nearest", SDL_SCALEMODE_NEAREST },
  { "linear",  SDL_SCALEMODE_LINEAR  },
  { NULL,      SDL_SCALEMODE_LINEAR }  /* fallback */
};

static JSValue scalemode2js(JSContext *js, SDL_ScaleMode mode){
  const scale_entry *it;
  for(it = k_scale_table; it->name; ++it)
    if(it->mode == mode) break;
  return JS_NewString(js, it->name ? it->name : "nearest");
}

SDL_ScaleMode js2SDL_ScaleMode(JSContext *js, JSValue v){
  if(JS_IsNull(v)) return SDL_SCALEMODE_NEAREST;
  const char *s = JS_ToCString(js, v);
  if(!s) return SDL_SCALEMODE_NEAREST;
  const scale_entry *it;
  for(it = k_scale_table; it->name; ++it)
    if(!strcmp(it->name, s)) break;
  JS_FreeCString(js, s);
  return it->mode;
}

// SDL_Surface free function
void SDL_Surface_free(JSRuntime *rt, SDL_Surface *s) {
  if (s->flags & SDL_SURFACE_PREALLOCATED)
    free(s->pixels);
  SDL_DestroySurface(s);
}

// Class definition
QJSCLASS(SDL_Surface,)

// SDL_Surface methods
JSC_CCALL(surface_blit,
  SDL_Surface *dst = js2SDL_Surface(js, self);

  irect dr = {0},  *pdr = NULL;
  if(!JS_IsNull(argv[0])){ dr = js2irect(js, argv[0]); pdr = &dr; }

  SDL_Surface *src = js2SDL_Surface(js, argv[1]);
  if (!src)
    return JS_ThrowReferenceError(js, "Argument must be a surface.");

  irect sr = {0}, *psr = NULL;
  if(!JS_IsNull(argv[2])){ sr = js2irect(js, argv[2]); psr = &sr; }

  SDL_ScaleMode mode = js2SDL_ScaleMode(js, argv[3]);
  
  SDL_SetSurfaceBlendMode(src, SDL_BLENDMODE_NONE);
  
  SDL_BlitSurfaceScaled(src, psr, dst, pdr, mode);
)

JSC_CCALL(surface_scale,
  SDL_Surface *src = js2SDL_Surface(js,self);
  HMM_Vec2 wh = js2vec2(js,argv[0]);
  SDL_ScaleMode mode = js2SDL_ScaleMode(js, argv[1]);
  SDL_Surface *new = SDL_ScaleSurface(src, wh.x, wh.y, mode);
  ret = SDL_Surface2js(js,new);
)

static SDL_PixelFormatDetails pdetails = {
    .format = SDL_PIXELFORMAT_RGBA8888, // Standard RGBA8888 format
    .bits_per_pixel = 32,              // 8 bits per channel, 4 channels = 32 bits
    .bytes_per_pixel = 4,              // 4 bytes per pixel
    .padding = {0, 0},                 // Unused padding
    .Rmask = 0xFF000000,               // Red mask
    .Gmask = 0x00FF0000,               // Green mask
    .Bmask = 0x0000FF00,               // Blue mask
    .Amask = 0x000000FF,               // Alpha mask
    .Rbits = 8,                        // 8 bits for Red
    .Gbits = 8,                        // 8 bits for Green
    .Bbits = 8,                        // 8 bits for Blue
    .Abits = 8,                        // 8 bits for Alpha
    .Rshift = 24,                      // Red shift
    .Gshift = 16,                      // Green shift
    .Bshift = 8,                       // Blue shift
    .Ashift = 0                        // Alpha shift
};

JSC_CCALL(surface_fill,
  SDL_Surface *src = js2SDL_Surface(js,self);
  colorf color = js2color(js,argv[0]);
  rect r = {
    .x = 0,
    .y = 0,
    .w = src->w,
    .h = src->h
  };
  SDL_FillSurfaceRect(src, &r, SDL_MapRGBA(&pdetails, NULL, color.r*255,color.g*255,color.b*255,color.a*255));
)

JSC_CCALL(surface_rect,
  SDL_Surface *dst = js2SDL_Surface(js,self);
  rect r = js2rect(js,argv[0]);  
  colorf color = js2color(js,argv[1]);
  SDL_FillSurfaceRect(dst,&r,SDL_MapRGBA(&pdetails,NULL, color.r*255,color.g*255,color.b*255,color.a*255));
) 

JSC_CCALL(surface_convert,
  SDL_Surface *surf = js2SDL_Surface(js,self);
  SDL_PixelFormat fmt = js2pixelformat(js, argv[0]);
  SDL_Surface *dst = SDL_ConvertSurface(surf, fmt);
  if (!dst) return JS_ThrowInternalError(js, "Convert failed: %s", SDL_GetError());
  
  return SDL_Surface2js(js, dst);
)

JSC_CCALL(surface_dup,
  SDL_Surface *surf = js2SDL_Surface(js,self);
  SDL_Surface *conv = SDL_DuplicateSurface(surf);
  if (!conv)
    return JS_ThrowReferenceError(js, "could not blit to dup'd surface: %s", SDL_GetError());

  return SDL_Surface2js(js,conv);
)

JSC_CCALL(surface_pixels,
  SDL_Surface *surf = js2SDL_Surface(js, self);

  int locked = 0;
  if (SDL_MUSTLOCK(surf))
    if (SDL_LockSurface(surf) < 0)
      return JS_ThrowReferenceError(js, "Lock surface failed: %s", SDL_GetError());
  
  size_t byte_size;
  if (SDL_ISPIXELFORMAT_FOURCC(surf->format)) {
    /* Planar/YUV formats: use BitsPerPixel to compute true size   */
    printf("FOURCC!!! Bits is %d\n", SDL_BYTESPERPIXEL(surf->format));
    byte_size = (size_t)surf->pitch * surf->h * SDL_BYTESPERPIXEL(surf->format);
  } else
    byte_size = (size_t)surf->pitch * surf->h;

  ret = js_new_blob_stoned_copy(js, surf->pixels, byte_size);

  if (locked) SDL_UnlockSurface(surf);
)

JSC_CCALL(surface_get_width,
  SDL_Surface *s = js2SDL_Surface(js,self);
  return JS_NewFloat64(js, s->w);
)

JSC_CCALL(surface_get_height,
  SDL_Surface *s = js2SDL_Surface(js,self);
  return JS_NewFloat64(js, s->h);
)

JSC_CCALL(surface_get_format,
  SDL_Surface *s = js2SDL_Surface(js,self);
  return pixelformat2js(js, s->format);
)

JSC_CCALL(surface_get_pitch,
  SDL_Surface *s = js2SDL_Surface(js,self);
  return JS_NewFloat64(js, s->pitch);
)

JSC_CCALL(surface_toJSON,
  SDL_Surface *surf = js2SDL_Surface(js,self);
  
  // Create the result object
  JSValue obj = JS_NewObject(js);
  
  // Add width and height
  JS_SetPropertyStr(js, obj, "width", JS_NewInt32(js, surf->w));
  JS_SetPropertyStr(js, obj, "height", JS_NewInt32(js, surf->h));
  
  // Add format
  JS_SetPropertyStr(js, obj, "format", pixelformat2js(js, surf->format));
  
  // Add pitch
  JS_SetPropertyStr(js, obj, "pitch", JS_NewInt32(js, surf->pitch));
  
  // Lock surface if needed
  int locked = 0;
  if (SDL_MUSTLOCK(surf)) {
    if (SDL_LockSurface(surf) < 0) {
      JS_FreeValue(js, obj);
      return JS_ThrowInternalError(js, "Lock surface failed: %s", SDL_GetError());
    }
    locked = 1;
  }
  
  // Add pixels as ArrayBuffer
  size_t byte_size = surf->pitch * surf->h;
  JSValue pixels = js_new_blob_stoned_copy(js, surf->pixels, byte_size);
  JS_SetPropertyStr(js, obj, "pixels", pixels);
  
  // Unlock if we locked
  if (locked)
    SDL_UnlockSurface(surf);
  
  return obj;
)

// Check for integer overflow in size calculations
static int check_size_overflow(size_t a, size_t b, size_t c, size_t *result)
{
  if (a > SIZE_MAX / b) return 1;
  size_t temp = a * b;
  if (temp > SIZE_MAX / c) return 1;
  *result = temp * c;
  return 0;
}

// Helper function for BC1/BC3 compression
static JSValue compress_bc_common(JSContext *js, JSValueConst *argv, int argc, int alpha_mode, const char *format_name)
{
  if (argc < 1)
    return JS_ThrowTypeError(js, "compress_%s requires an object argument", format_name);
  
  // Check if width/height properties exist
  JSValue width_val = JS_GetPropertyStr(js, argv[0], "width");
  JSValue height_val = JS_GetPropertyStr(js, argv[0], "height");
  
  if (JS_IsNull(width_val) || JS_IsNull(height_val)) {
    JS_FreeValue(js, width_val);
    JS_FreeValue(js, height_val);
    return JS_ThrowTypeError(js, "compress_%s requires width and height properties", format_name);
  }
  
  int width, height;
  if (JS_ToInt32(js, &width, width_val) < 0 || JS_ToInt32(js, &height, height_val) < 0) {
    JS_FreeValue(js, width_val);
    JS_FreeValue(js, height_val);
    return JS_ThrowTypeError(js, "width and height must be numbers");
  }
  JS_FreeValue(js, width_val);
  JS_FreeValue(js, height_val);
  
  if (width < 1 || height < 1)
    return JS_ThrowRangeError(js, "width and height must be at least 1");
  
  if (width % 4 != 0 || height % 4 != 0)
    return JS_ThrowRangeError(js, "Width and height must be multiples of 4 for BC compression");
  
  // Get pixel format
  JSValue format_val = JS_GetPropertyStr(js, argv[0], "format");
  SDL_PixelFormat format = js2pixelformat(js, format_val);
  JS_FreeValue(js, format_val);
  
  if (format == SDL_PIXELFORMAT_UNKNOWN)
    return JS_ThrowTypeError(js, "Invalid or missing pixel format");
  
  // Get pixels
  JSValue pixels_val = JS_GetPropertyStr(js, argv[0], "pixels");
  size_t pixel_len;
  void *pixel_data = js_get_blob_data(js, &pixel_len, pixels_val);
  
  if (!pixel_data) {
    JS_FreeValue(js, pixels_val);
    return JS_ThrowTypeError(js, "pixels property must be an ArrayBuffer");
  }
  
  // Validate buffer size
  int bytes_per_pixel = SDL_BYTESPERPIXEL(format);
  size_t required_size;
  if (check_size_overflow(width, height, bytes_per_pixel, &required_size)) {
    JS_FreeValue(js, pixels_val);
    return JS_ThrowRangeError(js, "Image dimensions too large");
  }
  
  if (pixel_len < required_size) {
    JS_FreeValue(js, pixels_val);
    return JS_ThrowRangeError(js, "pixels buffer too small for %dx%d format (need %zu bytes, got %zu)", 
                              width, height, required_size, pixel_len);
  }
  
  // Get high quality mode (default true)
  int high_quality = 1;
  if (argc > 1) {
    high_quality = JS_ToBool(js, argv[1]);
  }
  
  int mode = high_quality ? STB_DXT_HIGHQUAL : STB_DXT_NORMAL;
  
  // Calculate output size with overflow check
  int blocks_x = width / 4;
  int blocks_y = height / 4;
  int bytes_per_block = (alpha_mode == 0) ? 8 : 16; // BC1=8, BC3=16
  size_t output_size;
  if (check_size_overflow(blocks_x, blocks_y, bytes_per_block, &output_size)) {
    JS_FreeValue(js, pixels_val);
    return JS_ThrowRangeError(js, "Output dimensions too large");
  }
  
  // Allocate output buffer
  unsigned char *output = malloc(output_size);
  if (!output) {
    JS_FreeValue(js, pixels_val);
    return JS_ThrowOutOfMemory(js);
  }
  
  // Allocate RGBA conversion buffer
  size_t rgba_size;
  if (check_size_overflow(width, height, 4, &rgba_size)) {
    free(output);
    JS_FreeValue(js, pixels_val);
    return JS_ThrowRangeError(js, "RGBA buffer size too large");
  }
  
  unsigned char *rgba_data = malloc(rgba_size);
  if (!rgba_data) {
    free(output);
    JS_FreeValue(js, pixels_val);
    return JS_ThrowOutOfMemory(js);
  }
  
  // Convert to RGBA using SDL
  int convert_result = SDL_ConvertPixels(
    width, height,
    format, pixel_data, width * bytes_per_pixel,
    SDL_PIXELFORMAT_RGBA32, rgba_data, width * 4
  );
  
  JS_FreeValue(js, pixels_val);
  
  if (convert_result != 0) {
    free(output);
    free(rgba_data);
    return JS_ThrowInternalError(js, "Failed to convert pixels: %s", SDL_GetError());
  }
  
  // Compress blocks
  for (int by = 0; by < blocks_y; by++) {
    for (int bx = 0; bx < blocks_x; bx++) {
      unsigned char block[64]; // 4x4 RGBA = 64 bytes
      
      // Extract 4x4 block
      for (int y = 0; y < 4; y++) {
        for (int x = 0; x < 4; x++) {
          int src_x = bx * 4 + x;
          int src_y = by * 4 + y;
          int src_idx = (src_y * width + src_x) * 4;
          int dst_idx = (y * 4 + x) * 4;
          
          block[dst_idx + 0] = rgba_data[src_idx + 0];
          block[dst_idx + 1] = rgba_data[src_idx + 1];
          block[dst_idx + 2] = rgba_data[src_idx + 2];
          block[dst_idx + 3] = rgba_data[src_idx + 3];
        }
      }
      
      // Compress block
      int output_idx = (by * blocks_x + bx) * bytes_per_block;
      stb_compress_dxt_block(output + output_idx, block, alpha_mode, mode);
    }
  }
  
  free(rgba_data);
  
  // Create result object
  JSValue result = JS_NewObject(js);
  JS_SetPropertyStr(js, result, "width", JS_NewInt32(js, width));
  JS_SetPropertyStr(js, result, "height", JS_NewInt32(js, height));
  JS_SetPropertyStr(js, result, "format", JS_NewString(js, format_name));
  JS_SetPropertyStr(js, result, "pitch", JS_NewInt32(js, blocks_x * bytes_per_block));
  
  JSValue compressed_pixels = js_new_blob_stoned_copy(js, output, output_size);
  free(output);  // Free the output buffer after copying to blob
  JS_SetPropertyStr(js, result, "pixels", compressed_pixels);
  
  return result;
}

// BC1/DXT1 compression
JSC_CCALL(surface_compress_bc1,
  return compress_bc_common(js, argv, argc, 0, "bc1");
)

// BC3/DXT5 compression  
JSC_CCALL(surface_compress_bc3,
  return compress_bc_common(js, argv, argc, 1, "bc3");
)

// Generic helper for BC4/BC5 channel compression
static JSValue compress_bc_channels(JSContext *js, JSValueConst *argv, int argc, 
                                   int num_channels, const char *format_name,
                                   void (*compress_func)(unsigned char *dest, const unsigned char *src))
{
  if (argc < 1)
    return JS_ThrowTypeError(js, "compress_%s requires an object argument", format_name);
  
  // Check if width/height properties exist
  JSValue width_val = JS_GetPropertyStr(js, argv[0], "width");
  JSValue height_val = JS_GetPropertyStr(js, argv[0], "height");
  
  if (JS_IsNull(width_val) || JS_IsNull(height_val)) {
    JS_FreeValue(js, width_val);
    JS_FreeValue(js, height_val);
    return JS_ThrowTypeError(js, "compress_%s requires width and height properties", format_name);
  }
  
  int width, height;
  if (JS_ToInt32(js, &width, width_val) < 0 || JS_ToInt32(js, &height, height_val) < 0) {
    JS_FreeValue(js, width_val);
    JS_FreeValue(js, height_val);
    return JS_ThrowTypeError(js, "width and height must be numbers");
  }
  JS_FreeValue(js, width_val);
  JS_FreeValue(js, height_val);
  
  if (width < 1 || height < 1)
    return JS_ThrowRangeError(js, "width and height must be at least 1");
  
  if (width % 4 != 0 || height % 4 != 0)
    return JS_ThrowRangeError(js, "Width and height must be multiples of 4 for BC compression");
  
  // Get pixel format
  JSValue format_val = JS_GetPropertyStr(js, argv[0], "format");
  SDL_PixelFormat format = js2pixelformat(js, format_val);
  JS_FreeValue(js, format_val);
  
  if (format == SDL_PIXELFORMAT_UNKNOWN)
    return JS_ThrowTypeError(js, "Invalid or missing pixel format");
  
  // Get pixels
  JSValue pixels_val = JS_GetPropertyStr(js, argv[0], "pixels");
  size_t pixel_len;
  void *pixel_data = js_get_blob_data(js, &pixel_len, pixels_val);
  
  if (!pixel_data) {
    JS_FreeValue(js, pixels_val);
    return JS_ThrowTypeError(js, "pixels property must be an ArrayBuffer");
  }
  
  // Validate buffer size
  int bytes_per_pixel = SDL_BYTESPERPIXEL(format);
  if (bytes_per_pixel < num_channels) {
    JS_FreeValue(js, pixels_val);
    return JS_ThrowTypeError(js, "%s compression requires a format with at least %d channel(s)", 
                             format_name, num_channels);
  }
  
  size_t required_size;
  if (check_size_overflow(width, height, bytes_per_pixel, &required_size)) {
    JS_FreeValue(js, pixels_val);
    return JS_ThrowRangeError(js, "Image dimensions too large");
  }
  
  if (pixel_len < required_size) {
    JS_FreeValue(js, pixels_val);
    return JS_ThrowRangeError(js, "pixels buffer too small for %dx%d format (need %zu bytes, got %zu)", 
                              width, height, required_size, pixel_len);
  }
  
  // Calculate output size with overflow check
  int blocks_x = width / 4;
  int blocks_y = height / 4;
  int bytes_per_block = (num_channels == 1) ? 8 : 16; // BC4=8, BC5=16
  size_t output_size;
  if (check_size_overflow(blocks_x, blocks_y, bytes_per_block, &output_size)) {
    JS_FreeValue(js, pixels_val);
    return JS_ThrowRangeError(js, "Output dimensions too large");
  }
  
  // Allocate output buffer
  unsigned char *output = malloc(output_size);
  if (!output) {
    JS_FreeValue(js, pixels_val);
    return JS_ThrowOutOfMemory(js);
  }
  
  // Allocate channel extraction buffer
  size_t channel_size;
  if (check_size_overflow(width, height, num_channels, &channel_size)) {
    free(output);
    JS_FreeValue(js, pixels_val);
    return JS_ThrowRangeError(js, "Channel buffer size too large");
  }
  
  unsigned char *channel_data = malloc(channel_size);
  if (!channel_data) {
    free(output);
    JS_FreeValue(js, pixels_val);
    return JS_ThrowOutOfMemory(js);
  }
  
  // Extract channels
  for (int y = 0; y < height; y++) {
    for (int x = 0; x < width; x++) {
      int src_idx = (y * width + x) * bytes_per_pixel;
      int dst_idx = (y * width + x) * num_channels;
      
      // Extract first 'num_channels' channels
      for (int c = 0; c < num_channels; c++) {
        channel_data[dst_idx + c] = ((unsigned char*)pixel_data)[src_idx + c];
      }
    }
  }
  
  JS_FreeValue(js, pixels_val);
  
  // Compress blocks
  for (int by = 0; by < blocks_y; by++) {
    for (int bx = 0; bx < blocks_x; bx++) {
      unsigned char block[32]; // Max 4x4 * 2 channels = 32 bytes
      
      // Extract 4x4 block
      for (int y = 0; y < 4; y++) {
        for (int x = 0; x < 4; x++) {
          int src_x = bx * 4 + x;
          int src_y = by * 4 + y;
          int src_idx = (src_y * width + src_x) * num_channels;
          int dst_idx = (y * 4 + x) * num_channels;
          
          for (int c = 0; c < num_channels; c++) {
            block[dst_idx + c] = channel_data[src_idx + c];
          }
        }
      }
      
      // Compress block
      int output_idx = (by * blocks_x + bx) * bytes_per_block;
      compress_func(output + output_idx, block);
    }
  }
  
  free(channel_data);
  
  // Create result object
  JSValue result = JS_NewObject(js);
  JS_SetPropertyStr(js, result, "width", JS_NewInt32(js, width));
  JS_SetPropertyStr(js, result, "height", JS_NewInt32(js, height));
  JS_SetPropertyStr(js, result, "format", JS_NewString(js, format_name));
  JS_SetPropertyStr(js, result, "pitch", JS_NewInt32(js, blocks_x * bytes_per_block));
  
  JSValue compressed_pixels = js_new_blob_stoned_copy(js, output, output_size);
  free(output);  // Free the output buffer after copying to blob
  JS_SetPropertyStr(js, result, "pixels", compressed_pixels);
  
  return result;
}

// BC4 compression (single channel)
JSC_CCALL(surface_compress_bc4,
  return compress_bc_channels(js, argv, argc, 1, "bc4", stb_compress_bc4_block);
)

// BC5 compression (two channels)
JSC_CCALL(surface_compress_bc5,
  return compress_bc_channels(js, argv, argc, 2, "bc5", stb_compress_bc5_block);
)

// Constructor for SDL_Surface
JSC_CCALL(surface_constructor,
  if (argc < 1)
    return JS_ThrowTypeError(js, "Surface constructor requires an object argument");
  
  // Get width and height
  int width, height;
  JS_GETATOM(js, width, argv[0], width, number)
  JS_GETATOM(js, height, argv[0], height, number)
  
  if (!width || !height)
    return JS_ThrowTypeError(js, "Surface constructor requires width and height properties");
  
  // Check for pixel format
  SDL_PixelFormat format = SDL_PIXELFORMAT_RGBA32;  // default
  JSValue format_val = JS_GetPropertyStr(js, argv[0], "format");
  if (!JS_IsNull(format_val)) {
    format = js2pixelformat(js, format_val);
  }
  JS_FreeValue(js, format_val);
  
  // Check for pixel data
  JSValue pixels_val = JS_GetPropertyStr(js, argv[0], "pixels");
  if (!JS_IsNull(pixels_val)) {
    // Create surface from pixel data
    size_t len;
    void *raw = js_get_blob_data(js, &len, pixels_val);
    
    if (!raw) {
      JS_FreeValue(js, pixels_val);
      return JS_ThrowTypeError(js, "pixels property must be a stoned blob");
    }
    
    // Get pitch if provided, otherwise calculate it
    int pitch;
    JSValue pitch_val = JS_GetPropertyStr(js, argv[0], "pitch");
    if (!JS_IsNull(pitch_val)) {
      pitch = js2number(js, pitch_val);
      JS_FreeValue(js, pitch_val);
    } else {
      // Calculate pitch based on format
      int bytes_per_pixel = SDL_BYTESPERPIXEL(format);
      pitch = width * bytes_per_pixel;
    }
    
    // Copy the pixel data
    void *pixels_copy = malloc(len);
    if (!pixels_copy) {
      JS_FreeValue(js, pixels_val);
      return JS_ThrowOutOfMemory(js);
    }
    memcpy(pixels_copy, raw, len);
    
    SDL_Surface *surface = SDL_CreateSurfaceFrom(width, height, format, pixels_copy, pitch);
    if (!surface) {
      free(pixels_copy);
      JS_FreeValue(js, pixels_val);
      return JS_ThrowInternalError(js, "Failed to create surface from pixels: %s", SDL_GetError());
    }
    
    JS_FreeValue(js, pixels_val);
    ret = SDL_Surface2js(js, surface);
  } else {
    // Create blank surface
    SDL_Surface *surface = SDL_CreateSurface(width, height, format);
    if (!surface)
      return JS_ThrowInternalError(js, "Failed to create surface: %s", SDL_GetError());
      
    ret = SDL_Surface2js(js, surface);
  }
)

static const JSCFunctionListEntry js_SDL_Surface_funcs[] = {
  MIST_FUNC_DEF(surface, blit, 4),
  MIST_FUNC_DEF(surface, scale, 1),
  MIST_FUNC_DEF(surface, fill,1),
  MIST_FUNC_DEF(surface, rect,2),
  MIST_FUNC_DEF(surface, dup, 0),
  MIST_FUNC_DEF(surface, pixels, 0),
  MIST_FUNC_DEF(surface, convert, 1),
  MIST_FUNC_DEF(surface, toJSON, 0),
  JS_CGETSET_DEF("width", js_surface_get_width, NULL),
  JS_CGETSET_DEF("height", js_surface_get_height, NULL),
  JS_CGETSET_DEF("format", js_surface_get_format, NULL),
  JS_CGETSET_DEF("pitch", js_surface_get_pitch, NULL),
};

// Helper function to create SDL_Surface from image object
static SDL_Surface* image_to_surface(JSContext *js, JSValue img_obj)
{
  // Get width and height
  JSValue width_val = JS_GetPropertyStr(js, img_obj, "width");
  JSValue height_val = JS_GetPropertyStr(js, img_obj, "height");
  
  if (JS_IsNull(width_val) || JS_IsNull(height_val)) {
    JS_FreeValue(js, width_val);
    JS_FreeValue(js, height_val);
    return NULL;
  }
  
  int width, height;
  if (JS_ToInt32(js, &width, width_val) < 0 || JS_ToInt32(js, &height, height_val) < 0) {
    JS_FreeValue(js, width_val);
    JS_FreeValue(js, height_val);
    return NULL;
  }
  JS_FreeValue(js, width_val);
  JS_FreeValue(js, height_val);
  
  // Get format
  JSValue format_val = JS_GetPropertyStr(js, img_obj, "format");
  SDL_PixelFormat format = js2pixelformat(js, format_val);
  JS_FreeValue(js, format_val);
  
  if (format == SDL_PIXELFORMAT_UNKNOWN)
    format = SDL_PIXELFORMAT_RGBA32; // default
  
  // Get pixels
  JSValue pixels_val = JS_GetPropertyStr(js, img_obj, "pixels");
  size_t pixel_len;
  void *pixel_data = js_get_blob_data(js, &pixel_len, pixels_val);
  
  if (!pixel_data) {
    JS_FreeValue(js, pixels_val);
    return NULL;
  }
  
  // Get pitch (optional)
  int pitch;
  JSValue pitch_val = JS_GetPropertyStr(js, img_obj, "pitch");
  if (!JS_IsNull(pitch_val)) {
    pitch = js2number(js, pitch_val);
    JS_FreeValue(js, pitch_val);
  } else {
    pitch = width * SDL_BYTESPERPIXEL(format);
  }
  
  // Create a copy of pixel data since SDL_Surface will own it
  void *pixels_copy = malloc(pixel_len);
  if (!pixels_copy) {
    JS_FreeValue(js, pixels_val);
    return NULL;
  }
  memcpy(pixels_copy, pixel_data, pixel_len);
  JS_FreeValue(js, pixels_val);
  
  SDL_Surface *surface = SDL_CreateSurfaceFrom(width, height, format, pixels_copy, pitch);
  if (!surface) {
    free(pixels_copy);
    return NULL;
  }
  
  return surface;
}

// Helper function to convert SDL_Surface back to image object
static JSValue surface_to_image(JSContext *js, SDL_Surface *surf)
{
  JSValue obj = JS_NewObject(js);
  
  JS_SetPropertyStr(js, obj, "width", JS_NewInt32(js, surf->w));
  JS_SetPropertyStr(js, obj, "height", JS_NewInt32(js, surf->h));
  JS_SetPropertyStr(js, obj, "format", pixelformat2js(js, surf->format));
  JS_SetPropertyStr(js, obj, "pitch", JS_NewInt32(js, surf->pitch));
  
  // Lock surface if needed
  int locked = 0;
  if (SDL_MUSTLOCK(surf)) {
    if (SDL_LockSurface(surf) < 0) {
      JS_FreeValue(js, obj);
      return JS_NULL;
    }
    locked = 1;
  }
  
  // Add pixels as stoned blob
  size_t byte_size = surf->pitch * surf->h;
  JSValue pixels = js_new_blob_stoned_copy(js, surf->pixels, byte_size);
  JS_SetPropertyStr(js, obj, "pixels", pixels);
  
  // Unlock if we locked
  if (locked)
    SDL_UnlockSurface(surf);
  
  // Add depth and hdr properties for completeness
  JS_SetPropertyStr(js, obj, "depth", JS_NewInt32(js, SDL_BITSPERPIXEL(surf->format)));
  JS_SetPropertyStr(js, obj, "hdr", JS_FALSE);
  
  return obj;
}

// Scale function for image objects
JSC_CCALL(surface_scale_img,
  if (argc < 2)
    return JS_ThrowTypeError(js, "scale requires image and options objects");
  
  SDL_Surface *src = image_to_surface(js, argv[0]);
  if (!src)
    return JS_ThrowTypeError(js, "First argument must be a valid image object");
  
  // Get width and height from options
  JSValue width_val = JS_GetPropertyStr(js, argv[1], "width");
  JSValue height_val = JS_GetPropertyStr(js, argv[1], "height");
  
  int new_width = src->w, new_height = src->h;
  if (!JS_IsNull(width_val))
    JS_ToInt32(js, &new_width, width_val);
  if (!JS_IsNull(height_val))
    JS_ToInt32(js, &new_height, height_val);
  
  JS_FreeValue(js, width_val);
  JS_FreeValue(js, height_val);
  
  // Get scale mode
  JSValue mode_val = JS_GetPropertyStr(js, argv[1], "mode");
  SDL_ScaleMode mode = js2SDL_ScaleMode(js, mode_val);
  JS_FreeValue(js, mode_val);
  
  SDL_Surface *dst = SDL_ScaleSurface(src, new_width, new_height, mode);
  SDL_DestroySurface(src);
  
  if (!dst)
    return JS_ThrowInternalError(js, "Scale failed: %s", SDL_GetError());
  
  JSValue result = surface_to_image(js, dst);
  SDL_DestroySurface(dst);
  return result;
)

// Fill function for image objects
JSC_CCALL(surface_fill_img,
  if (argc < 2)
    return JS_ThrowTypeError(js, "fill requires image and color");
  
  SDL_Surface *surf = image_to_surface(js, argv[0]);
  if (!surf)
    return JS_ThrowTypeError(js, "First argument must be a valid image object");
  
  colorf color = js2color(js, argv[1]);
  rect r = {
    .x = 0,
    .y = 0,
    .w = surf->w,
    .h = surf->h
  };
  
  SDL_FillSurfaceRect(surf, &r, SDL_MapRGBA(&pdetails, NULL, color.r*255, color.g*255, color.b*255, color.a*255));
  
  JSValue result = surface_to_image(js, surf);
  SDL_DestroySurface(surf);
  return result;
)

// Rect function for image objects
JSC_CCALL(surface_rect_img,
  if (argc < 3)
    return JS_ThrowTypeError(js, "rect requires image, rectangle, and color");
  
  SDL_Surface *surf = image_to_surface(js, argv[0]);
  if (!surf)
    return JS_ThrowTypeError(js, "First argument must be a valid image object");
  
  rect r = js2rect(js, argv[1]);
  colorf color = js2color(js, argv[2]);
  
  SDL_FillSurfaceRect(surf, &r, SDL_MapRGBA(&pdetails, NULL, color.r*255, color.g*255, color.b*255, color.a*255));
  
  JSValue result = surface_to_image(js, surf);
  SDL_DestroySurface(surf);
  return result;
)

// Blit function for image objects
JSC_CCALL(surface_blit_img,
  if (argc < 2)
    return JS_ThrowTypeError(js, "blit requires destination and source images");
  
  SDL_Surface *dst = image_to_surface(js, argv[0]);
  if (!dst)
    return JS_ThrowTypeError(js, "First argument must be a valid destination image");
  
  SDL_Surface *src = image_to_surface(js, argv[1]);
  if (!src) {
    SDL_DestroySurface(dst);
    return JS_ThrowTypeError(js, "Second argument must be a valid source image");
  }
  
  irect dr = {0}, *pdr = NULL;
  if (argc > 2 && !JS_IsNull(argv[2])) {
    dr = js2irect(js, argv[2]);
    pdr = &dr;
  }
  
  irect sr = {0}, *psr = NULL;
  if (argc > 3 && !JS_IsNull(argv[3])) {
    sr = js2irect(js, argv[3]);
    psr = &sr;
  }
  
  SDL_ScaleMode mode = SDL_SCALEMODE_LINEAR;
  if (argc > 4)
    mode = js2SDL_ScaleMode(js, argv[4]);
  
  SDL_SetSurfaceBlendMode(src, SDL_BLENDMODE_NONE);
  SDL_BlitSurfaceScaled(src, psr, dst, pdr, mode);
  
  SDL_DestroySurface(src);
  JSValue result = surface_to_image(js, dst);
  SDL_DestroySurface(dst);
  return result;
)

// Duplicate function for image objects
JSC_CCALL(surface_dup_img,
  if (argc < 1)
    return JS_ThrowTypeError(js, "dup requires an image object");
  
  SDL_Surface *surf = image_to_surface(js, argv[0]);
  if (!surf)
    return JS_ThrowTypeError(js, "Argument must be a valid image object");
  
  SDL_Surface *dup = SDL_DuplicateSurface(surf);
  SDL_DestroySurface(surf);
  
  if (!dup)
    return JS_ThrowInternalError(js, "Duplicate failed: %s", SDL_GetError());
  
  JSValue result = surface_to_image(js, dup);
  SDL_DestroySurface(dup);
  return result;
)

// Generic convert function for pixel format/colorspace conversion
JSC_CCALL(surface_convert_generic,
  if (argc < 2)
    return JS_ThrowTypeError(js, "convert requires source and conversion objects");
  
  // Parse source object
  int src_width, src_height;
  JS_GETATOM(js, src_width, argv[0], width, number)
  JS_GETATOM(js, src_height, argv[0], height, number)
  
  if (!src_width || !src_height)
    return JS_ThrowTypeError(js, "source object requires width and height");
  
  // Get source format
  JSValue src_format_val = JS_GetPropertyStr(js, argv[0], "format");
  SDL_PixelFormat src_format = js2pixelformat(js, src_format_val);
  JS_FreeValue(js, src_format_val);
  
  if (src_format == SDL_PIXELFORMAT_UNKNOWN)
    return JS_ThrowTypeError(js, "source object requires valid format");
  
  // Get source pixels
  JSValue src_pixels_val = JS_GetPropertyStr(js, argv[0], "pixels");
  size_t src_len;
  void *src_pixels = js_get_blob_data(js, &src_len, src_pixels_val);
  
  if (!src_pixels) {
    JS_FreeValue(js, src_pixels_val);
    return JS_ThrowTypeError(js, "source pixels must be an ArrayBuffer");
  }
  
  // Get source pitch (optional, calculate if not provided)
  int src_pitch;
  JSValue src_pitch_val = JS_GetPropertyStr(js, argv[0], "pitch");
  if (!JS_IsNull(src_pitch_val)) {
    src_pitch = js2number(js, src_pitch_val);
    JS_FreeValue(js, src_pitch_val);
  } else {
    src_pitch = src_width * SDL_BYTESPERPIXEL(src_format);
  }
  
  // Get source colorspace (optional)
  JSValue src_colorspace_val = JS_GetPropertyStr(js, argv[0], "colorspace");
  SDL_Colorspace src_colorspace = SDL_COLORSPACE_SRGB; // default
  if (!JS_IsNull(src_colorspace_val)) {
    // For now, we'll use a simple numeric value for colorspace
    int colorspace_num;
    if (JS_ToInt32(js, &colorspace_num, src_colorspace_val) == 0) {
      src_colorspace = (SDL_Colorspace)colorspace_num;
    }
  }
  JS_FreeValue(js, src_colorspace_val);
  
  // Parse conversion object
  JSValue dst_format_val = JS_GetPropertyStr(js, argv[1], "format");
  SDL_PixelFormat dst_format = js2pixelformat(js, dst_format_val);
  JS_FreeValue(js, dst_format_val);
  
  if (dst_format == SDL_PIXELFORMAT_UNKNOWN)
    return JS_ThrowTypeError(js, "conversion object requires valid format");
  
  // Get destination pitch (optional)
  int dst_pitch;
  JSValue dst_pitch_val = JS_GetPropertyStr(js, argv[1], "pitch");
  if (!JS_IsNull(dst_pitch_val)) {
    dst_pitch = js2number(js, dst_pitch_val);
    JS_FreeValue(js, dst_pitch_val);
  } else {
    dst_pitch = src_width * SDL_BYTESPERPIXEL(dst_format);
  }
  
  // Get destination colorspace (optional)
  JSValue dst_colorspace_val = JS_GetPropertyStr(js, argv[1], "colorspace");
  SDL_Colorspace dst_colorspace = SDL_COLORSPACE_SRGB; // default
  if (!JS_IsNull(dst_colorspace_val)) {
    int colorspace_num;
    if (JS_ToInt32(js, &colorspace_num, dst_colorspace_val) == 0) {
      dst_colorspace = (SDL_Colorspace)colorspace_num;
    }
  }
  JS_FreeValue(js, dst_colorspace_val);
  
  // Calculate destination buffer size
  size_t dst_size = dst_pitch * src_height;
  void *dst_pixels = malloc(dst_size);
  if (!dst_pixels) {
    JS_FreeValue(js, src_pixels_val);
    return JS_ThrowOutOfMemory(js);
  }
  
  // Check if we have colorspace info for both source and dest
  bool has_src_colorspace = !JS_IsNull(JS_GetPropertyStr(js, argv[0], "colorspace"));
  bool has_dst_colorspace = !JS_IsNull(JS_GetPropertyStr(js, argv[1], "colorspace"));
  
  bool success;
  if (has_src_colorspace || has_dst_colorspace) {
    // Use SDL_ConvertPixelsAndColorspace
    success = SDL_ConvertPixelsAndColorspace(
      src_width, src_height,
      src_format, src_colorspace, 0, src_pixels, src_pitch,
      dst_format, dst_colorspace, 0, dst_pixels, dst_pitch
    );
  } else {
    // Use SDL_ConvertPixels
    success = SDL_ConvertPixels(
      src_width, src_height,
      src_format, src_pixels, src_pitch,
      dst_format, dst_pixels, dst_pitch
    );
  }
  
  JS_FreeValue(js, src_pixels_val);
  
  if (!success) {
    free(dst_pixels);
    return JS_ThrowInternalError(js, "Pixel conversion failed: %s", SDL_GetError());
  }
  
  // Create result image object
  JSValue result = JS_NewObject(js);
  JS_SetPropertyStr(js, result, "width", JS_NewInt32(js, src_width));
  JS_SetPropertyStr(js, result, "height", JS_NewInt32(js, src_height));
  JS_SetPropertyStr(js, result, "format", pixelformat2js(js, dst_format));
  JS_SetPropertyStr(js, result, "pitch", JS_NewInt32(js, dst_pitch));
  
  JSValue pixels = js_new_blob_stoned_copy(js, dst_pixels, dst_size);
  free(dst_pixels);
  JS_SetPropertyStr(js, result, "pixels", pixels);
  
  // Add depth and hdr for consistency
  JS_SetPropertyStr(js, result, "depth", JS_NewInt32(js, SDL_BITSPERPIXEL(dst_format)));
  JS_SetPropertyStr(js, result, "hdr", JS_FALSE);
  
  return result;
)

JSValue js_sdl_surface_use(JSContext *js)
{
  QJSCLASSPREP_FUNCS(SDL_Surface)
  
  // Add the surface constructor
  JSValue ctor = JS_NewCFunction2(js, js_surface_constructor, "surface", 1, JS_CFUNC_constructor, 0);
  
  JSValue proto = JS_GetClassProto(js, js_SDL_Surface_id);
  JS_SetConstructor(js, ctor, proto);
  JS_FreeValue(js, proto);
  
  // Add the generic convert function as a property on the constructor
  JS_SetPropertyStr(js, ctor, "convert", JS_NewCFunction(js, js_surface_convert_generic, "convert", 2));
  
  // Add the compression functions as static methods on the constructor
  JS_SetPropertyStr(js, ctor, "compress_bc1", JS_NewCFunction(js, js_surface_compress_bc1, "compress_bc1", 2));
  JS_SetPropertyStr(js, ctor, "compress_bc3", JS_NewCFunction(js, js_surface_compress_bc3, "compress_bc3", 2));
  JS_SetPropertyStr(js, ctor, "compress_bc4", JS_NewCFunction(js, js_surface_compress_bc4, "compress_bc4", 1));
  JS_SetPropertyStr(js, ctor, "compress_bc5", JS_NewCFunction(js, js_surface_compress_bc5, "compress_bc5", 1));
  
  // Add standalone image manipulation functions
  JS_SetPropertyStr(js, ctor, "scale", JS_NewCFunction(js, js_surface_scale_img, "scale", 2));
  JS_SetPropertyStr(js, ctor, "fill", JS_NewCFunction(js, js_surface_fill_img, "fill", 2));
  JS_SetPropertyStr(js, ctor, "rect", JS_NewCFunction(js, js_surface_rect_img, "rect", 3));
  JS_SetPropertyStr(js, ctor, "blit", JS_NewCFunction(js, js_surface_blit_img, "blit", 5));
  JS_SetPropertyStr(js, ctor, "dup", JS_NewCFunction(js, js_surface_dup_img, "dup", 1));
  
  return ctor;
}