#include "qjs_geometry.h"
#include "qjs_blob.h"
#include "jsffi.h"
#include "qjs_common.h"

#include <SDL3/SDL.h>
#include <math.h>
#include "HandmadeMath.h"
#include "font.h"
#include "sprite.h"
#include "transform.h"
#include "stb_ds.h"

// GEOMETRY FUNCTIONS

JSC_CCALL(geometry_rect_intersection,
  rect a = js2rect(js,argv[0]);
  rect b = js2rect(js,argv[1]);
  rect c;
  SDL_GetRectIntersectionFloat(&a, &b, &c);
  return rect2js(js,c);
)

JSC_CCALL(geometry_rect_intersects,
  rect a = js2rect(js,argv[0]);
  rect b = js2rect(js,argv[1]);
  return JS_NewBool(js, SDL_HasRectIntersectionFloat(&a,&b));
)

JSC_CCALL(geometry_rect_inside,
  rect inner = js2rect(js,argv[0]);
  rect outer = js2rect(js,argv[1]);
  return JS_NewBool(js,
         inner.x >= outer.x &&
         inner.x + inner.w <= outer.x + outer.w &&
         inner.y >= outer.y &&
         inner.y + inner.h <= outer.y + outer.h
         );
)

JSC_CCALL(geometry_rect_random,
  rect a = js2rect(js,argv[0]);
  return vec22js(js,(HMM_Vec2){
    a.x + rand_range(js,-0.5,0.5)*a.w,
    a.y + rand_range(js,-0.5,0.5)*a.h
  });
)

JSC_CCALL(geometry_rect_point_inside,
  rect a = js2rect(js,argv[0]);
  HMM_Vec2 p = js2vec2(js,argv[1]);
  return JS_NewBool(js,p.x >= a.x && p.x <= a.x+a.w && p.y <= a.y+a.h && p.y >= a.y);
)

JSC_CCALL(geometry_cwh2rect,
  HMM_Vec2 c = js2vec2(js,argv[0]);
  HMM_Vec2 wh = js2vec2(js,argv[1]);
  rect r;
  r.x = c.x;
  r.y = c.y;
  r.w = wh.x;
  r.h = wh.y;
  return rect2js(js,r);
)

JSC_CCALL(geometry_rect_pos,
  rect r = js2rect(js,argv[0]);
  return vec22js(js,(HMM_Vec2){
    .x = r.x,
    .y = r.y
  });
)

JSC_CCALL(geometry_rect_move,
  rect r = js2rect(js,argv[0]);
  HMM_Vec2 move = js2vec2(js,argv[1]);
  r.x += move.x;
  r.y += move.y;
  return rect2js(js,r);
)

JSC_CCALL(geometry_rect_expand,
  rect a = js2rect(js,argv[0]);
  rect b = js2rect(js,argv[1]);
  rect c = {0};
  c.x = fmin(a.x,b.x);
  c.y = fmin(a.y,b.y);
  c.w = fmax(a.x+a.w,b.x+b.w);
  c.h = fmax(a.y+a.h,b.y+b.h);
  return rect2js(js,c);
)

// Helper functions for geometry operations



static inline void add_quad(text_vert **verts, rect *restrict src, rect *restrict dst)
{
  text_vert v = (text_vert){
    .pos = (HMM_Vec2){dst->x, dst->y},
    .uv = (HMM_Vec2){src->x,src->y},
    .color = (HMM_Vec4){1,1,1,1}
  };
  arrput(*verts, v);

  v = (text_vert){
    .pos = (HMM_Vec2){dst->x+dst->w, dst->y},
    .uv = (HMM_Vec2){src->x+src->w,src->y},
    .color = (HMM_Vec4){1,1,1,1}
  };
  arrput(*verts, v);

  v = (text_vert){
    .pos = (HMM_Vec2){dst->x, dst->y+dst->h},
    .uv = (HMM_Vec2){src->x,src->y+src->h},
    .color = (HMM_Vec4){1,1,1,1}
  };
  arrput(*verts, v);

  v = (text_vert){
    .pos = (HMM_Vec2){dst->x+dst->w, dst->y+dst->h},
    .uv = (HMM_Vec2){src->x+src->w,src->y+src->h},
    .color = (HMM_Vec4){1,1,1,1}
  };
  arrput(*verts, v);
}

static inline void tile_region(text_vert **verts, rect src_uv, rect dst, float tex_w, float tex_h, bool tile_x, bool tile_y)
{
  // Convert the incoming UV rect into pixel coords
  rect src_px = {
    .x = src_uv.x * tex_w,
    .y = src_uv.y * tex_h,
    .w = src_uv.w * tex_w,
    .h = src_uv.h * tex_h
  };

  // If src_px or dst is degenerate, early out
  if (src_px.w <= 0.0f || src_px.h <= 0.0f || dst.w <= 0.0f || dst.h <= 0.0f)
    return;

  // How many full tiles horizontally/vertically?
  // If not tiling in a dimension, we treat it as exactly 1 tile (no leftover).
  float cols_f, rows_f;
  float remain_wf = 0.0f;
  float remain_hf = 0.0f;

  if (tile_x) {
    remain_wf = modff(dst.w / src_px.w, &cols_f);
  } else {
    // Only 1 "column" covering entire width, no leftover
    cols_f = 1.0f;
    remain_wf = 0.0f;
  }

  if (tile_y) {
    remain_hf = modff(dst.h / src_px.h, &rows_f);
  } else {
    // Only 1 "row" covering entire height, no leftover
    rows_f = 1.0f;
    remain_hf = 0.0f;
  }

  int cols = (int)cols_f;
  int rows = (int)rows_f;

  // The leftover portion in screen coords (pixels)
  float remain_dst_w = remain_wf * src_px.w;
  float remain_dst_h = remain_hf * src_px.h;

  // Build the UV rect for a "full" tile
  rect src_full = src_uv;

  // Partial leftover in UV
  float remain_src_w_uv = remain_dst_w / tex_w;
  float remain_src_h_uv = remain_dst_h / tex_h;

  // For partial leftover in X dimension
  rect src_partial_x = src_full;
  src_partial_x.w = remain_src_w_uv;

  // For partial leftover in Y dimension
  rect src_partial_y = src_full;
  src_partial_y.h = remain_src_h_uv;

  // For partial leftover in both X & Y
  rect src_partial_xy = src_full;
  src_partial_xy.w = remain_src_w_uv;
  src_partial_xy.h = remain_src_h_uv;

  // Each tile is drawn 1:1 in screen coords
  float tile_w = tile_x ? src_px.w : dst.w;  // If not tiling horizontally, match the entire dst width
  float tile_h = tile_y ? src_px.h : dst.h;  // If not tiling vertically, match the entire dst height

  rect curr_dst;
  curr_dst.w = tile_w;
  curr_dst.h = tile_h;
  curr_dst.y = dst.y;

  // Loop over rows
  for (int y = 0; y < rows; y++) {
    curr_dst.x = dst.x;

    // Loop over columns
    for (int x = 0; x < cols; x++) {
      add_quad(verts, &src_full, &curr_dst);
      curr_dst.x += tile_w;
    }

    // Right-side leftover tile (only if tile_x is true)
    if (tile_x && remain_dst_w > 0.0f) {
      rect partial_dst = {
        .x = curr_dst.x, .y = curr_dst.y,
        .w = remain_dst_w, .h = tile_h
      };
      add_quad(verts, &src_partial_x, &partial_dst);
    }
    curr_dst.y += tile_h;
  }

  // Bottom leftover row (only if tile_y is true)
  if (tile_y && remain_dst_h > 0.0f) {
    rect partial_row_dst;
    partial_row_dst.w = tile_w;
    partial_row_dst.h = remain_dst_h;
    partial_row_dst.y = curr_dst.y;
    partial_row_dst.x = dst.x;

    // Full columns in leftover row
    for (int x = 0; x < cols; x++) {
      add_quad(verts, &src_partial_y, &partial_row_dst);
      partial_row_dst.x += tile_w;
    }

    // Partial leftover corner (both X & Y leftover)
    if (tile_x && remain_dst_w > 0.0f) {
      rect partial_corner_dst = {
        .x = partial_row_dst.x, .y = partial_row_dst.y,
        .w = remain_dst_w,     .h = remain_dst_h
      };
      add_quad(verts, &src_partial_xy, &partial_corner_dst);
    }
  }
}

JSC_CCALL(gpu_slice9,
  JSValue jstex = argv[0];
  rect dst = js2rect(js, argv[1]);

  // Full texture in UV coords
  rect src = {
    .x = 0, .y = 0,
    .w = 1, .h = 1
  };

  // The "slice" LRTB in PIXELS, but we convert to UV below
  lrtb src_slice = js2lrtb(js, argv[2]);
  lrtb dst_slice = src_slice;

  HMM_Vec2 size;
  JS_GETPROP(js, size.x, jstex, width,  number)
  JS_GETPROP(js, size.y, jstex, height, number)
  
  JSValue info = argv[3];
  int tile_top, tile_bottom, tile_left, tile_right, center_x, center_y;
  JS_GETPROP(js,tile_top, info, tile_top, bool)
  JS_GETPROP(js,tile_bottom,info,tile_bottom,bool)
  JS_GETPROP(js,tile_left,info,tile_left,bool)  
  JS_GETPROP(js,tile_right,info,tile_right,bool)
  JS_GETPROP(js, center_x, info, tile_center_x, bool)
  JS_GETPROP(js, center_y, info, tile_center_y, bool)
  
  // Convert the slice edges from pixel to UV
  src_slice.l /= size.x;
  src_slice.r /= size.x;
  src_slice.t /= size.y;
  src_slice.b /= size.y;

  text_vert *verts = NULL;
  rect curr_src;
  rect curr_dst;

  // bottom-left corner (single quad)
  curr_src = src;
  curr_src.w = src_slice.l;
  curr_src.h = src_slice.b;

  curr_dst = dst;
  curr_dst.w = dst_slice.l;
  curr_dst.h = dst_slice.b;
  add_quad(&verts, &curr_src, &curr_dst);

  // top-left corner (single quad)
  curr_src = src;
  curr_src.x = src.x;
  curr_src.y = src.y + src.h - src_slice.t;
  curr_src.w = src_slice.l;
  curr_src.h = src_slice.t;

  curr_dst = dst;
  curr_dst.x = dst.x;
  curr_dst.y = dst.y + dst.h - dst_slice.t;
  curr_dst.w = dst_slice.l;
  curr_dst.h = dst_slice.t;
  add_quad(&verts, &curr_src, &curr_dst);

  // bottom-right corner (single quad)
  curr_src = src;
  curr_src.x = src.x + src.w - src_slice.r;
  curr_src.y = src.y;
  curr_src.w = src_slice.r;
  curr_src.h = src_slice.b;

  curr_dst = dst;
  curr_dst.x = dst.x + dst.w - dst_slice.r;
  curr_dst.y = dst.y;
  curr_dst.w = dst_slice.r;
  curr_dst.h = dst_slice.b;
  add_quad(&verts, &curr_src, &curr_dst);

  // top-right corner (single quad)
  curr_src = src;
  curr_src.x = src.x + src.w - src_slice.r;
  curr_src.y = src.y + src.h - src_slice.t;
  curr_src.w = src_slice.r;
  curr_src.h = src_slice.t;

  curr_dst = dst;
  curr_dst.x = dst.x + dst.w - dst_slice.r;
  curr_dst.y = dst.y + dst.h - dst_slice.t;
  curr_dst.w = dst_slice.r;
  curr_dst.h = dst_slice.t;
  add_quad(&verts, &curr_src, &curr_dst);

  // left bar (tiled)
  curr_src = src;
  curr_src.x = src.x;
  curr_src.y = src.y + src_slice.b;
  curr_src.w = src_slice.l;
  curr_src.h = src.h - src_slice.t - src_slice.b;

  curr_dst = dst;
  curr_dst.x = dst.x;
  curr_dst.y = dst.y + dst_slice.b;
  curr_dst.w = dst_slice.l;
  curr_dst.h = dst.h - dst_slice.t - dst_slice.b;
  tile_region(&verts, curr_src, curr_dst, size.x, size.y,tile_left,tile_left);

  // right bar (tiled)
  curr_src = src;
  curr_src.x = src.x + src.w - src_slice.r;
  curr_src.y = src.y + src_slice.b;
  curr_src.w = src_slice.r;
  curr_src.h = src.h - src_slice.t - src_slice.b;

  curr_dst = dst;
  curr_dst.x = dst.x + dst.w - dst_slice.r;
  curr_dst.y = dst.y + dst_slice.b;
  curr_dst.w = dst_slice.r;
  curr_dst.h = dst.h - dst_slice.t - dst_slice.b;
  tile_region(&verts, curr_src, curr_dst, size.x, size.y,tile_right,tile_right);

  // bottom bar (tiled)
  curr_src = src;
  curr_src.x = src.x + src_slice.l;
  curr_src.y = src.y;
  curr_src.w = src.w - src_slice.l - src_slice.r;
  curr_src.h = src_slice.b;

  curr_dst = dst;
  curr_dst.x = dst.x + dst_slice.l;
  curr_dst.y = dst.y;
  curr_dst.w = dst.w - dst_slice.l - dst_slice.r;
  curr_dst.h = dst_slice.b;
  tile_region(&verts, curr_src, curr_dst, size.x, size.y,tile_bottom,tile_bottom);

  // top bar (tiled)
  curr_src = src;
  curr_src.x = src.x + src_slice.l;
  curr_src.y = src.y + src.h - src_slice.t;
  curr_src.w = src.w - src_slice.l - src_slice.r;
  curr_src.h = src_slice.t;

  curr_dst = dst;
  curr_dst.x = dst.x + dst_slice.l;
  curr_dst.y = dst.y + dst.h - dst_slice.t;
  curr_dst.w = dst.w - dst_slice.l - dst_slice.r;
  curr_dst.h = dst_slice.t;
  tile_region(&verts, curr_src, curr_dst, size.x, size.y,tile_top,tile_top);

  // center (tiled)
  curr_src = src;
  curr_src.x = src.x + src_slice.l;
  curr_src.y = src.y + src_slice.b;
  curr_src.w = src.w - src_slice.l - src_slice.r;
  curr_src.h = src.h - src_slice.t - src_slice.b;

  curr_dst = dst;
  curr_dst.x = dst.x + dst_slice.l;
  curr_dst.y = dst.y + dst_slice.b;
  curr_dst.w = dst.w - dst_slice.l - dst_slice.r;
  curr_dst.h = dst.h - dst_slice.t - dst_slice.b;
  tile_region(&verts, curr_src, curr_dst, size.x, size.y, center_x,center_y);

  JSValue mesh = quads_to_mesh(js, verts);
  arrfree(verts);
  ret = mesh;
)

JSC_CCALL(gpu_tile,
  HMM_Vec2 size;
  JSValue jstex = argv[0];
  JS_GETATOM(js,size.x,jstex,width,number)
  JS_GETATOM(js, size.y, jstex, height, number)

  rect src_pixels = js2rect(js, argv[1]); // 'src' as pixel dimensions
  rect dst = js2rect(js, argv[2]);        // 'dst' as screen coords
  
  int tilex, tiley;
  JSValue jstile = argv[3];
  JS_GETPROP(js,tilex,jstile,repeat_x,bool)
  JS_GETPROP(js,tiley,jstile,repeat_y,bool)
  
  text_vert *verts = NULL;
  tile_region(&verts, src_pixels, dst, size.x, size.y,tilex,tiley);
  ret = quads_to_mesh(js,verts);
  arrfree(verts);
)

// Sprite mesh generation functions
typedef struct {
  JSValue val;
  void *ptr;
  size_t size;
  int need_new;
} BufferCheckResult;

static BufferCheckResult get_or_extend_buffer(
  JSContext *js,
  JSValue old_mesh,
  const char *prop,
  size_t needed_size,
  int type,
  int elements_per_item,
  int copy,
  int index
) {
  BufferCheckResult res = { JS_NULL, NULL, 0, 0 };
  if (!JS_IsNull(old_mesh)) {
    JSValue old_buf = JS_GetPropertyStr(js, old_mesh, prop);
    if (!JS_IsNull(old_buf)) {
      size_t old_size;
      void *data = get_gpu_buffer(js, old_buf, NULL, &old_size);
      if (data && old_size >= needed_size) {
        // Old buffer is large enough
        res.val = old_buf; // keep it
        res.ptr = data;
        res.size = old_size;
        return res;
      }
      JS_FreeValue(js, old_buf);
    }
  }
  // If we reach here, we need a new buffer
  res.need_new = 1;
  return res;
}

static HMM_Vec3 base_quad[4] = {
  {0.0,0.0,1.0},
  {1.0,0.0,1.0},
  {0.0,1.0,1.0},
  {1.0,1.0,1.0}
};

struct quad_buffers {
  HMM_Vec2 *pos;
  HMM_Vec2 *uv;
  HMM_Vec4 *color;
  int verts;
};

struct quad_buffers quad_buffers_new(int verts)
{
  struct quad_buffers b;
  b.verts = verts;
  b.pos = malloc(sizeof(HMM_Vec2)*verts);
  b.uv = malloc(sizeof(HMM_Vec2)*verts);
  b.color = malloc(sizeof(HMM_Vec4)*verts);
  return b;
}

int sort_sprite(const sprite *a, const sprite *b)
{
  if (a->layer != b->layer) return a->layer - b->layer;
  
  if (a->pos.Y != b->pos.Y)
    return (b->pos.Y - a->pos.Y);
    
  if (JS_VALUE_GET_PTR(a->image) != JS_VALUE_GET_PTR(b->image))
    return JS_VALUE_GET_PTR(a->image) < JS_VALUE_GET_PTR(b->image) ? -1 : 1;  
  return 0;
}

JSC_CCALL(geometry_rect_transform,
  // argv[0] = world‐space rect
  rect r        = js2rect(js, argv[0]);
  // argv[1] = world_to_projection (16 floats)
  size_t byte_len;
  float *data12 = js_get_blob_data(js, &byte_len, argv[1]);
  HMM_Mat4 wp; memcpy(wp.Elements, data12, sizeof(wp.Elements));

  // make our two corners at z=0
  HMM_Vec4 p0 = { r.x,         r.y,         0.0f, 1.0f };
  HMM_Vec4 p1 = { r.x + r.w,   r.y + r.h,   0.0f, 1.0f };

  // transform into clip space
  HMM_Vec4 t0 = HMM_MulM4V4(wp, p0);
  HMM_Vec4 t1 = HMM_MulM4V4(wp, p1);

  // perspective divide → NDC
  float ndc_x0 = t0.X / t0.W, ndc_y0 = t0.Y / t0.W;
  float ndc_x1 = t1.X / t1.W, ndc_y1 = t1.Y / t1.W;

  // NDC → UV
  float u0 = ndc_x0 * 0.5f + 0.5f;
  float v0 = ndc_y0 * 0.5f + 0.5f;
  float u1 = ndc_x1 * 0.5f + 0.5f;
  float v1 = ndc_y1 * 0.5f + 0.5f;

  // (Optionally multiply by pixel‐size here, or do that in JS)
  rect newrect = {
    .x = u0,
    .y = v0,
    .w = u1 - u0,
    .h = v1 - v0
  };

  return rect2js(js, newrect);
)

JSC_CCALL(geometry_tilemap_to_data,
  JSValue tilemap_obj = argv[0];
  
  // Get tilemap properties
  double offset_x, offset_y, size_x, size_y;
  double pos_x = 0, pos_y = 0;
  JS_GETPROP(js, offset_x, tilemap_obj, offset_x, number)
  JS_GETPROP(js, offset_y, tilemap_obj, offset_y, number)
  JS_GETPROP(js, size_x, tilemap_obj, size_x, number)
  JS_GETPROP(js, size_y, tilemap_obj, size_y, number)
  
  // Get position properties (optional, default to 0)
  JSValue pos_x_val = JS_GetPropertyStr(js, tilemap_obj, "pos_x");
  if (!JS_IsNull(pos_x_val)) {
    JS_ToFloat64(js, &pos_x, pos_x_val);
  }
  JS_FreeValue(js, pos_x_val);
  
  JSValue pos_y_val = JS_GetPropertyStr(js, tilemap_obj, "pos_y");
  if (!JS_IsNull(pos_y_val)) {
    JS_ToFloat64(js, &pos_y, pos_y_val);
  }
  JS_FreeValue(js, pos_y_val);
  
  JSValue tiles_array = JS_GetPropertyStr(js, tilemap_obj, "tiles");
  if (!JS_IsArray(js, tiles_array)) {
    JS_FreeValue(js, tiles_array);
    return JS_ThrowTypeError(js, "tilemap.tiles must be an array");
  }
  
  // Count tiles
  int tile_count = 0;
  int tiles_len = JS_ArrayLength(js, tiles_array);
  for (int x = 0; x < tiles_len; x++) {
    JSValue col = JS_GetPropertyUint32(js, tiles_array, x);
    if (JS_IsArray(js, col)) {
      int col_len = JS_ArrayLength(js, col);
      for (int y = 0; y < col_len; y++) {
        JSValue tile = JS_GetPropertyUint32(js, col, y);
        if (!JS_IsNull(tile) && !JS_IsNull(tile)) {
          tile_count++;
        }
        JS_FreeValue(js, tile);
      }
    }
    JS_FreeValue(js, col);
  }
  
  if (tile_count == 0) {
    JS_FreeValue(js, tiles_array);
    return JS_NewObject(js);
  }
  
  // Allocate buffers - 4 vertices per tile
  int vertex_count = tile_count * 4;
  int index_count = tile_count * 6;
  
  float *xy_data = malloc(vertex_count * 2 * sizeof(float));
  float *uv_data = malloc(vertex_count * 2 * sizeof(float));
  SDL_FColor *color_data = malloc(vertex_count * sizeof(SDL_FColor));
  uint16_t *index_data = malloc(index_count * sizeof(uint16_t));
  
  // Generate vertices
  int vertex_idx = 0;
  int index_idx = 0;
  
  for (int x = 0; x < tiles_len; x++) {
    JSValue col = JS_GetPropertyUint32(js, tiles_array, x);
    if (JS_IsArray(js, col)) {
      int col_len = JS_ArrayLength(js, col);
      for (int y = 0; y < col_len; y++) {
        JSValue tile = JS_GetPropertyUint32(js, col, y);
        if (!JS_IsNull(tile) && !JS_IsNull(tile)) {
          // Calculate world position
          // x and y are array indices, need to convert to logical coordinates
          float logical_x = x + offset_x;
          float logical_y = y + offset_y;
          float world_x = logical_x * size_x + pos_x;
          float world_y = logical_y * size_y + pos_y;
          
          // Set vertex positions (4 corners of the tile)
          int base = vertex_idx * 2;
          xy_data[base + 0] = world_x;
          xy_data[base + 1] = world_y;
          xy_data[base + 2] = world_x + size_x;
          xy_data[base + 3] = world_y;
          xy_data[base + 4] = world_x;
          xy_data[base + 5] = world_y + size_y;
          xy_data[base + 6] = world_x + size_x;
          xy_data[base + 7] = world_y + size_y;
          
          // Set UVs (normalized 0-1 for now)
          uv_data[base + 0] = 0;      uv_data[base + 1] = 1.0f; // now samples the TOP of the image
          uv_data[base + 2] = 1.0f;   uv_data[base + 3] = 1.0f;
          uv_data[base + 4] = 0;      uv_data[base + 5] = 0;
          uv_data[base + 6] = 1.0f;   uv_data[base + 7] = 0;

          
          // Set colors (check if tile has color property)
          SDL_FColor default_color = {1.0f, 1.0f, 1.0f, 1.0f};
          if (JS_IsObject(tile)) {
            JSValue color_val = JS_GetPropertyStr(js, tile, "color");
            if (!JS_IsNull(color_val)) {
              HMM_Vec4 color = js2color(js, color_val);
              default_color.r = color.r;
              default_color.g = color.g;
              default_color.b = color.b;
              default_color.a = color.a;
              JS_FreeValue(js, color_val);
            }
          }
          
          for (int i = 0; i < 4; i++) {
            color_data[vertex_idx + i] = default_color;
          }
          
          // Set indices (two triangles per tile)
          uint16_t base_idx = vertex_idx;
          index_data[index_idx++] = base_idx + 0;
          index_data[index_idx++] = base_idx + 1;
          index_data[index_idx++] = base_idx + 2;
          index_data[index_idx++] = base_idx + 1;
          index_data[index_idx++] = base_idx + 3;
          index_data[index_idx++] = base_idx + 2;
          
          vertex_idx += 4;
        }
        JS_FreeValue(js, tile);
      }
    }
    JS_FreeValue(js, col);
  }
  
  JS_FreeValue(js, tiles_array);
  
  // Create result object with blob data
  ret = JS_NewObject(js);
  
  // Create blobs for each data type
  JSValue xy_blob = js_new_blob_stoned_copy(js, xy_data, vertex_count * 2 * sizeof(float));
  JSValue uv_blob = js_new_blob_stoned_copy(js, uv_data, vertex_count * 2 * sizeof(float));
  JSValue color_blob = js_new_blob_stoned_copy(js, color_data, vertex_count * sizeof(SDL_FColor));
  JSValue index_blob = js_new_blob_stoned_copy(js, index_data, index_count * sizeof(uint16_t));
  
  JS_SetPropertyStr(js, ret, "xy", xy_blob);
  JS_SetPropertyStr(js, ret, "xy_stride", JS_NewInt32(js, 2 * sizeof(float)));
  JS_SetPropertyStr(js, ret, "uv", uv_blob);
  JS_SetPropertyStr(js, ret, "uv_stride", JS_NewInt32(js, 2 * sizeof(float)));
  JS_SetPropertyStr(js, ret, "color", color_blob);
  JS_SetPropertyStr(js, ret, "color_stride", JS_NewInt32(js, sizeof(SDL_FColor)));
  JS_SetPropertyStr(js, ret, "indices", index_blob);
  JS_SetPropertyStr(js, ret, "num_vertices", JS_NewInt32(js, vertex_count));
  JS_SetPropertyStr(js, ret, "num_indices", JS_NewInt32(js, index_count));
  JS_SetPropertyStr(js, ret, "size_indices", JS_NewInt32(js, 2)); // using uint16_t
  
  free(xy_data);
  free(uv_data);
  free(color_data);
  free(index_data);
)

static void print_buffers(float *xy_data, float *uv_data, SDL_FColor *color_data, uint16_t *index_data, int vertex_count, int index_count)
{
  printf("=== GEOMETRY BUFFERS ===\n");
  
  printf("XY Data (%d vertices):\n", vertex_count);
  for (int i = 0; i < vertex_count; i++) {
    printf("  Vertex %d: x=%.2f, y=%.2f\n", i, xy_data[i*2], xy_data[i*2+1]);
  }
  
  printf("\nUV Data:\n");
  for (int i = 0; i < vertex_count; i++) {
    printf("  Vertex %d: u=%.4f, v=%.4f\n", i, uv_data[i*2], uv_data[i*2+1]);
  }
  
  printf("\nColor Data:\n");
  for (int i = 0; i < vertex_count; i++) {
    printf("  Vertex %d: r=%.2f, g=%.2f, b=%.2f, a=%.2f\n", 
           i, color_data[i].r, color_data[i].g, color_data[i].b, color_data[i].a);
  }
  
  printf("\nIndex Data (%d indices):\n", index_count);
  for (int i = 0; i < index_count; i += 3) {
    printf("  Triangle %d: %d, %d, %d\n", i/3, index_data[i], index_data[i+1], index_data[i+2]);
  }
  
  printf("========================\n");
}

JSC_CCALL(geometry_sprites_to_data,
  JSValue sprites_array = argv[0];
  if (!JS_IsArray(js, sprites_array)) {
    return JS_ThrowTypeError(js, "sprites must be an array");
  }
  
  int sprite_count = JS_ArrayLength(js, sprites_array);
  if (sprite_count == 0) {
    return JS_NewObject(js);
  }
  
  // Allocate buffers - 4 vertices per sprite
  int vertex_count = sprite_count * 4;
  int index_count = sprite_count * 6;
  
  float *xy_data = malloc(vertex_count * 2 * sizeof(float));
  float *uv_data = malloc(vertex_count * 2 * sizeof(float));
  SDL_FColor *color_data = malloc(vertex_count * sizeof(SDL_FColor));
  uint16_t *index_data = malloc(index_count * sizeof(uint16_t));
  
  // Generate vertices
  int vertex_idx = 0;
  int index_idx = 0;
  
  for (int i = 0; i < sprite_count; i++) {
    JSValue sprite = JS_GetPropertyUint32(js, sprites_array, i);
    
    // Get sprite properties
    JSValue pos_val = JS_GetPropertyStr(js, sprite, "pos");
    JSValue texture_val = JS_GetPropertyStr(js, sprite, "texture");
    JSValue color_val = JS_GetPropertyStr(js, sprite, "color");
    
    double width = 32, height = 32, anchor_x = 0.5, anchor_y = 0.5;
    
    // Try to get width/height from sprite first, otherwise use texture dimensions
    JSValue width_val = JS_GetPropertyStr(js, sprite, "width");
    if (!JS_IsNull(width_val)) {
      JS_ToFloat64(js, &width, width_val);
    } else if (!JS_IsNull(texture_val)) {
      // Get width from texture
      JSValue texture_width = JS_GetPropertyStr(js, texture_val, "width");
      if (!JS_IsNull(texture_width)) {
        JS_ToFloat64(js, &width, texture_width);
      }
      JS_FreeValue(js, texture_width);
    }
    JS_FreeValue(js, width_val);
    
    JSValue height_val = JS_GetPropertyStr(js, sprite, "height");
    if (!JS_IsNull(height_val)) {
      JS_ToFloat64(js, &height, height_val);
    } else if (!JS_IsNull(texture_val)) {
      // Get height from texture
      JSValue texture_height = JS_GetPropertyStr(js, texture_val, "height");
      if (!JS_IsNull(texture_height)) {
        JS_ToFloat64(js, &height, texture_height);
      }
      JS_FreeValue(js, texture_height);
    }
    JS_FreeValue(js, height_val);
    
    JSValue anchor_x_val = JS_GetPropertyStr(js, sprite, "anchor_x");
    if (!JS_IsNull(anchor_x_val)) {
      JS_ToFloat64(js, &anchor_x, anchor_x_val);
    }
    JS_FreeValue(js, anchor_x_val);
    
    JSValue anchor_y_val = JS_GetPropertyStr(js, sprite, "anchor_y");
    if (!JS_IsNull(anchor_y_val)) {
      JS_ToFloat64(js, &anchor_y, anchor_y_val);
    }
    JS_FreeValue(js, anchor_y_val);
    
    HMM_Vec2 pos = js2vec2(js, pos_val);
    
    // Calculate sprite corners with anchor
    float half_w = width * 0.5f;
    float half_h = height * 0.5f;
    float anchor_offset_x = width * anchor_x - half_w;
    float anchor_offset_y = height * anchor_y - half_h;
    
    float left = pos.x - half_w - anchor_offset_x;
    float right = pos.x + half_w - anchor_offset_x;
    float bottom = pos.y - half_h - anchor_offset_y;
    float top = pos.y + half_h - anchor_offset_y;
    
    // Set vertex positions (4 corners of the sprite)
    int base = vertex_idx * 2;
    xy_data[base + 0] = left;   // bottom-left
    xy_data[base + 1] = bottom;
    xy_data[base + 2] = right;  // bottom-right
    xy_data[base + 3] = bottom;
    xy_data[base + 4] = left;   // top-left
    xy_data[base + 5] = top;
    xy_data[base + 6] = right;  // top-right
    xy_data[base + 7] = top;
    
    // Get UV coordinates from texture (if available)
    if (!JS_IsNull(texture_val)) {
      JSValue rect_val = JS_GetPropertyStr(js, texture_val, "rect");
      if (!JS_IsNull(rect_val)) {
        rect uv_rect = js2rect(js, rect_val);
        
        // Get texture dimensions to normalize pixel coordinates
        double tex_width = 1.0, tex_height = 1.0;
        JSValue texture_width = JS_GetPropertyStr(js, texture_val, "width");
        JSValue texture_height = JS_GetPropertyStr(js, texture_val, "height");
        if (!JS_IsNull(texture_width)) {
          JS_ToFloat64(js, &tex_width, texture_width);
        }
        if (!JS_IsNull(texture_height)) {
          JS_ToFloat64(js, &tex_height, texture_height);
        }
        JS_FreeValue(js, texture_width);
        JS_FreeValue(js, texture_height);
        
        // The rect contains pixel coordinates, normalize them
        float u0 = uv_rect.x / tex_width;
        float v0 = uv_rect.y / tex_height;
        float u1 = (uv_rect.x + uv_rect.w) / tex_width;
        float v1 = (uv_rect.y + uv_rect.h) / tex_height;
        
        // Set UVs based on normalized texture rect
        uv_data[base + 0] = u0;  uv_data[base + 1] = v1;  // bottom-left
        uv_data[base + 2] = u1;  uv_data[base + 3] = v1;  // bottom-right
        uv_data[base + 4] = u0;  uv_data[base + 5] = v0;  // top-left
        uv_data[base + 6] = u1;  uv_data[base + 7] = v0;  // top-right
        JS_FreeValue(js, rect_val);
      } else {
        // Default UVs (0-1)
        uv_data[base + 0] = 0.0f;  uv_data[base + 1] = 1.0f;
        uv_data[base + 2] = 1.0f;  uv_data[base + 3] = 1.0f;
        uv_data[base + 4] = 0.0f;  uv_data[base + 5] = 0.0f;
        uv_data[base + 6] = 1.0f;  uv_data[base + 7] = 0.0f;
      }
    } else {
      // Default UVs (0-1)
      uv_data[base + 0] = 0.0f;  uv_data[base + 1] = 1.0f;
      uv_data[base + 2] = 1.0f;  uv_data[base + 3] = 1.0f;
      uv_data[base + 4] = 0.0f;  uv_data[base + 5] = 0.0f;
      uv_data[base + 6] = 1.0f;  uv_data[base + 7] = 0.0f;
    }
    
    // Set colors
    SDL_FColor default_color = {1.0f, 1.0f, 1.0f, 1.0f};
    if (!JS_IsNull(color_val)) {
      HMM_Vec4 color = js2color(js, color_val);
      default_color.r = color.r;
      default_color.g = color.g;
      default_color.b = color.b;
      default_color.a = color.a;
    }
    
    for (int j = 0; j < 4; j++) {
      color_data[vertex_idx + j] = default_color;
    }
    
    // Set indices (two triangles per sprite)
    uint16_t base_idx = vertex_idx;
    index_data[index_idx++] = base_idx + 0;  // triangle 1: 0,1,2
    index_data[index_idx++] = base_idx + 1;
    index_data[index_idx++] = base_idx + 2;
    index_data[index_idx++] = base_idx + 1;  // triangle 2: 1,3,2
    index_data[index_idx++] = base_idx + 3;
    index_data[index_idx++] = base_idx + 2;
    
    vertex_idx += 4;
    
    JS_FreeValue(js, pos_val);
    JS_FreeValue(js, texture_val);
    JS_FreeValue(js, color_val);
    JS_FreeValue(js, sprite);
  }
  
  // Create result object with blob data
  ret = JS_NewObject(js);
  
  // Create blobs for each data type
  JSValue xy_blob = js_new_blob_stoned_copy(js, xy_data, vertex_count * 2 * sizeof(float));
  JSValue uv_blob = js_new_blob_stoned_copy(js, uv_data, vertex_count * 2 * sizeof(float));
  JSValue color_blob = js_new_blob_stoned_copy(js, color_data, vertex_count * sizeof(SDL_FColor));
  JSValue index_blob = js_new_blob_stoned_copy(js, index_data, index_count * sizeof(uint16_t));
  
  JS_SetPropertyStr(js, ret, "xy", xy_blob);
  JS_SetPropertyStr(js, ret, "xy_stride", JS_NewInt32(js, 2 * sizeof(float)));
  JS_SetPropertyStr(js, ret, "uv", uv_blob);
  JS_SetPropertyStr(js, ret, "uv_stride", JS_NewInt32(js, 2 * sizeof(float)));
  JS_SetPropertyStr(js, ret, "color", color_blob);
  JS_SetPropertyStr(js, ret, "color_stride", JS_NewInt32(js, sizeof(SDL_FColor)));
  JS_SetPropertyStr(js, ret, "indices", index_blob);
  JS_SetPropertyStr(js, ret, "num_vertices", JS_NewInt32(js, vertex_count));
  JS_SetPropertyStr(js, ret, "num_indices", JS_NewInt32(js, index_count));
  JS_SetPropertyStr(js, ret, "size_indices", JS_NewInt32(js, 2)); // using uint16_t
  
  free(xy_data);
  free(uv_data);
  free(color_data);
  free(index_data);
)

JSC_CCALL(geometry_transform_xy_blob,
  // argv[0] = xy blob (contains vertex positions as float pairs)
  // argv[1] = camera transform parameters [a, c, e, f]
  
  JSValue xy_blob = argv[0];
  if (!js_is_blob(js, xy_blob)) {
    return JS_ThrowTypeError(js, "First argument must be an XY blob");
  }
  
  JSValue camera_params = argv[1];
  if (!JS_IsArray(js, camera_params) || JS_ArrayLength(js, camera_params) != 4) {
    return JS_ThrowTypeError(js, "Second argument must be an array of 4 camera transform parameters [a, c, e, f]");
  }
  
  // Get camera transform parameters
  double a, c, e, f;
  JSValue a_val = JS_GetPropertyUint32(js, camera_params, 0);
  JSValue c_val = JS_GetPropertyUint32(js, camera_params, 1);
  JSValue e_val = JS_GetPropertyUint32(js, camera_params, 2);
  JSValue f_val = JS_GetPropertyUint32(js, camera_params, 3);
  
  JS_ToFloat64(js, &a, a_val);
  JS_ToFloat64(js, &c, c_val);
  JS_ToFloat64(js, &e, e_val);
  JS_ToFloat64(js, &f, f_val);
  
  JS_FreeValue(js, a_val);
  JS_FreeValue(js, c_val);
  JS_FreeValue(js, e_val);
  JS_FreeValue(js, f_val);
  
  // Get blob data
  size_t xy_size;
  float *xy_data = (float*)js_get_blob_data(js, &xy_size, xy_blob);
  if (!xy_data) {
    return JS_ThrowTypeError(js, "Failed to get XY blob data");
  }
  
  // Calculate number of vertices (each vertex has 2 floats: x, y)
  int vertex_count = xy_size / (2 * sizeof(float));
  if (vertex_count * 2 * sizeof(float) != xy_size) {
    return JS_ThrowTypeError(js, "XY blob size is not a multiple of vertex size");
  }
  
  // Allocate new buffer for transformed coordinates
  float *transformed_xy = malloc(xy_size);
  if (!transformed_xy) {
    return JS_ThrowTypeError(js, "Failed to allocate memory for transformed coordinates");
  }
  
  // Apply camera transformation to each vertex
  for (int i = 0; i < vertex_count; i++) {
    float world_x = xy_data[i * 2 + 0];
    float world_y = xy_data[i * 2 + 1];
    
    // Apply 2D affine transformation: screen = world * camera_matrix
    float screen_x = a * world_x + c;
    float screen_y = e * world_y + f;
    
    transformed_xy[i * 2 + 0] = screen_x;
    transformed_xy[i * 2 + 1] = screen_y;
  }
  
  // Create new blob with transformed data
  JSValue transformed_blob = js_new_blob_stoned_copy(js, transformed_xy, xy_size);
  
  free(transformed_xy);
  
  ret = transformed_blob;
)

JSC_CCALL(geometry_array_blob,
  JSValue arr = argv[0];
  size_t len = JS_ArrayLength(js,arr);
  float data[len];
  for (int i = 0; i < len; i++) {
    JSValue val = JS_GetPropertyUint32(js,arr,i);
    data[i] = js2number(js, val);
    JS_FreeValue(js,val);
  }
  
  return js_new_blob_stoned_copy(js, data, sizeof(float)*len);
)

JSC_CCALL(geometry_make_quad_indices,
  int quads;
  if (JS_ToInt32(js, &quads, argv[0]) < 0) {
    return JS_ThrowTypeError(js, "quads must be a number");
  }
  
  if (quads < 0) {
    return JS_ThrowTypeError(js, "quads must be >= 0");
  }
  
  int index_count = quads * 6;
  uint16_t *index_data = malloc(index_count * sizeof(uint16_t));
  
  int index_idx = 0;
  for (int i = 0; i < quads; i++) {
    uint16_t base_idx = i * 4;
    index_data[index_idx++] = base_idx + 0;
    index_data[index_idx++] = base_idx + 1;
    index_data[index_idx++] = base_idx + 2;
    index_data[index_idx++] = base_idx + 1;
    index_data[index_idx++] = base_idx + 3;
    index_data[index_idx++] = base_idx + 2;
  }
  
  JSValue result = js_new_blob_stoned_copy(js, index_data, index_count * sizeof(uint16_t));
  free(index_data);
  return result;
)

JSC_CCALL(geometry_make_rect_quad,
  rect r = js2rect(js, argv[0]);
  
  // Optional UV rect (default to 0,0,1,1)
  rect uv = {0, 0, 1, 1};
  if (argc > 1 && !JS_IsNull(argv[1])) {
    uv = js2rect(js, argv[1]);
  }
  
  // Optional color (default to white)
  SDL_FColor color = {1.0f, 1.0f, 1.0f, 1.0f};
  if (argc > 2 && !JS_IsNull(argv[2])) {
    HMM_Vec4 c = js2color(js, argv[2]);
    color.r = c.r;
    color.g = c.g;
    color.b = c.b;
    color.a = c.a;
  }
  
  // Allocate buffers for 1 quad (4 vertices)
  int vertex_count = 4;
  int index_count = 6;
  
  float xy_data[vertex_count*2];
  float uv_data[vertex_count*2];
  SDL_FColor color_data[vertex_count];
  
  // Set vertex positions (4 corners)
  xy_data[0] = r.x;        xy_data[1] = r.y;         // bottom-left
  xy_data[2] = r.x + r.w;  xy_data[3] = r.y;         // bottom-right  
  xy_data[4] = r.x;        xy_data[5] = r.y + r.h;   // top-left
  xy_data[6] = r.x + r.w;  xy_data[7] = r.y + r.h;   // top-right
  
  // Set UV coordinates
  uv_data[0] = uv.x;         uv_data[1] = uv.y + uv.h;  // bottom-left
  uv_data[2] = uv.x + uv.w;  uv_data[3] = uv.y + uv.h;  // bottom-right
  uv_data[4] = uv.x;         uv_data[5] = uv.y;         // top-left
  uv_data[6] = uv.x + uv.w;  uv_data[7] = uv.y;         // top-right
  
  // Set colors
  for (int i = 0; i < 4; i++)
    color_data[i] = color;
  
  // Create result object
  ret = JS_NewObject(js);
  
  JSValue xy_blob = js_new_blob_stoned_copy(js, xy_data, vertex_count * 2 * sizeof(float));
  JSValue uv_blob = js_new_blob_stoned_copy(js, uv_data, vertex_count * 2 * sizeof(float));
  JSValue color_blob = js_new_blob_stoned_copy(js, color_data, vertex_count * sizeof(SDL_FColor));
  
  JS_SetPropertyStr(js, ret, "xy", xy_blob);
  JS_SetPropertyStr(js, ret, "xy_stride", JS_NewInt32(js, 2 * sizeof(float)));
  JS_SetPropertyStr(js, ret, "uv", uv_blob);
  JS_SetPropertyStr(js, ret, "uv_stride", JS_NewInt32(js, 2 * sizeof(float)));
  JS_SetPropertyStr(js, ret, "color", color_blob);
  JS_SetPropertyStr(js, ret, "color_stride", JS_NewInt32(js, sizeof(SDL_FColor)));
  JS_SetPropertyStr(js, ret, "num_vertices", JS_NewInt32(js, vertex_count));
)

static const JSCFunctionListEntry js_geometry_funcs[] = {
  MIST_FUNC_DEF(geometry, rect_intersection, 2),
  MIST_FUNC_DEF(geometry, rect_intersects, 2),
  MIST_FUNC_DEF(geometry, rect_expand, 2),
  MIST_FUNC_DEF(geometry, rect_inside, 2),
  MIST_FUNC_DEF(geometry, rect_random, 1),
  MIST_FUNC_DEF(geometry, cwh2rect, 2),
  MIST_FUNC_DEF(geometry, rect_point_inside, 2),
  MIST_FUNC_DEF(geometry, rect_pos, 1),
  MIST_FUNC_DEF(geometry, rect_move, 2),
  MIST_FUNC_DEF(geometry, rect_transform, 2),
  MIST_FUNC_DEF(geometry, tilemap_to_data, 1),
  MIST_FUNC_DEF(geometry, sprites_to_data, 1),
  MIST_FUNC_DEF(geometry, transform_xy_blob, 2),
  MIST_FUNC_DEF(gpu, tile, 4),
  MIST_FUNC_DEF(gpu, slice9, 4),
  MIST_FUNC_DEF(geometry, array_blob, 2),
  MIST_FUNC_DEF(geometry, make_quad_indices, 1),
  MIST_FUNC_DEF(geometry, make_rect_quad, 3),
};

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