#include "cell_qoi.h"
#include "qoi.h"
#include "qjs_blob.h"
#include "qjs_sdl.h"
#include <SDL3/SDL.h>
#include <string.h>

// Helper function to 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;
}

// QOI compression/encoding
JSValue js_qoi_encode(JSContext *js, JSValue this_val, int argc, JSValueConst *argv)
{
  if (argc < 1)
    return JS_ThrowTypeError(js, "compress_qoi requires an object argument");
  
  // 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_qoi requires width and height properties");
  }
  
  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");
  
  // Get pixel format
  JSValue format_val = JS_GetPropertyStr(js, argv[0], "format");
  SDL_PixelFormat format = js2SDL_PixelFormat(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);
  JS_FreeValue(js, pixels_val);
  
  if (!pixel_data)
    return JS_ThrowTypeError(js, "pixels property must be a stoned blob");
  
  // 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)
    return JS_ThrowRangeError(js, "pixels buffer too small for %dx%d format (need %zu bytes, got %zu)", 
                              width, height, required_size, pixel_len);
  
  // Get colorspace (optional, default to sRGB)
  int colorspace = 0; // QOI_SRGB
  if (argc > 1) {
    colorspace = JS_ToBool(js, argv[1]);
  }
  
  // Determine number of channels based on format
  int channels = SDL_ISPIXELFORMAT_ALPHA(format) ? 4 : 3;
  
  // Prepare QOI descriptor
  qoi_desc desc = {
    .width = width,
    .height = height,
    .channels = channels,
    .colorspace = colorspace
  };
  
  // Encode to QOI
  int out_len;
  void *qoi_data = qoi_encode(pixel_data, &desc, &out_len);
  
  if (!qoi_data)
    return JS_ThrowInternalError(js, "QOI encoding failed");
  
  // 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, "qoi"));
  JS_SetPropertyStr(js, result, "channels", JS_NewInt32(js, channels));
  JS_SetPropertyStr(js, result, "colorspace", JS_NewInt32(js, colorspace));
  
  JSValue compressed_pixels = js_new_blob_stoned_copy(js, qoi_data, out_len);
  free(qoi_data);  // Free the QOI buffer after copying to blob
  JS_SetPropertyStr(js, result, "pixels", compressed_pixels);
  
  return result;
}

// QOI decompression/decoding
JSValue js_qoi_decode(JSContext *js, JSValue this_val, int argc, JSValueConst *argv)
{
  size_t len;
  void *raw = js_get_blob_data(js, &len, argv[0]);
  if (!raw) return JS_ThrowReferenceError(js, "could not get QOI data from array buffer");

  qoi_desc desc;
  void *data = qoi_decode(raw, len, &desc, 0); // 0 means use channels from file
  
  if (!data)
    return JS_NULL; // Return null if not valid QOI
    
  // QOI always decodes to either RGB or RGBA based on the file's channel count
  int channels = desc.channels;
  int pitch = desc.width * channels;
  size_t pixels_size = pitch * desc.height;
  
  // If it's RGB, convert to RGBA for consistency
  void *rgba_data = data;
  if (channels == 3) {
    rgba_data = malloc(desc.width * desc.height * 4);
    if (!rgba_data) {
      free(data);
      return JS_ThrowOutOfMemory(js);
    }
    
    // Convert RGB to RGBA
    unsigned char *src = (unsigned char*)data;
    unsigned char *dst = (unsigned char*)rgba_data;
    for (int i = 0; i < desc.width * desc.height; i++) {
      dst[i*4] = src[i*3];
      dst[i*4+1] = src[i*3+1];
      dst[i*4+2] = src[i*3+2];
      dst[i*4+3] = 255;
    }
    free(data);
    pitch = desc.width * 4;
    pixels_size = pitch * desc.height;
  }
  
  // Create JS object with surface data
  JSValue obj = JS_NewObject(js);
  JS_SetPropertyStr(js, obj, "width", JS_NewInt32(js, desc.width));
  JS_SetPropertyStr(js, obj, "height", JS_NewInt32(js, desc.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, rgba_data, pixels_size));
  JS_SetPropertyStr(js, obj, "depth", JS_NewInt32(js, 8));
  JS_SetPropertyStr(js, obj, "hdr", JS_NewBool(js, 0));
  JS_SetPropertyStr(js, obj, "colorspace", JS_NewInt32(js, desc.colorspace));
  
  free(rgba_data);
  return obj;
}

static const JSCFunctionListEntry js_qoi_funcs[] = {
  MIST_FUNC_DEF(qoi, encode, 1),
  MIST_FUNC_DEF(qoi, decode, 1)
};

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