#include "qjs_renderer.h"

#include <SDL3/SDL.h>

#include <math.h>
#include <string.h>
#include <stdlib.h>
#include "HandmadeMath.h"

QJSCLASS(SDL_Renderer,)

rect transform_rect(rect in, HMM_Mat3 *t)
{
  HMM_Vec3 bottom_left = (HMM_Vec3){in.x,in.y,1.0};
  HMM_Vec3 transformed_bl = HMM_MulM3V3(*t, bottom_left);
  in.x = transformed_bl.x;
  in.y = transformed_bl.y;
  in.y = in.y - in.h; // should be done for any platform that draws rectangles from top left
  return in;
}

HMM_Vec2 transform_point(SDL_Renderer *ren, HMM_Vec2 in, HMM_Mat3 *t)
{
  rect logical;
  SDL_GetRenderLogicalPresentationRect(ren, &logical);
  in.y *= -1;
  in.y += logical.h;
  in.x -= t->Columns[2].x;
  in.y -= t->Columns[2].y;
  return in;
}

JSC_CCALL(SDL_Renderer_clear,
  SDL_Renderer *renderer = js2SDL_Renderer(js,self);
  SDL_RenderClear(renderer);
)

JSC_CCALL(SDL_Renderer_present,
  SDL_Renderer *ren = js2SDL_Renderer(js,self);
  SDL_RenderPresent(ren);
)

JSC_CCALL(SDL_Renderer_draw_color,
  SDL_Renderer *renderer = js2SDL_Renderer(js,self);
  colorf color = js2color(js,argv[0]);
  SDL_SetRenderDrawColorFloat(renderer, color.r,color.g,color.b,color.a);
)

JSC_CCALL(SDL_Renderer_rect,
  SDL_Renderer *r = js2SDL_Renderer(js,self);
  if (!JS_IsNull(argv[1])) {
    colorf color = js2color(js,argv[1]);  
    SDL_SetRenderDrawColorFloat(r, color.r, color.g, color.b, color.a);
  }

  if (JS_IsArray(js,argv[0])) {
    int len = JS_ArrayLength(js,argv[0]);
    rect rects[len];
    for (int i = 0; i < len; i++) {
      JSValue val = JS_GetPropertyUint32(js,argv[0],i);
      rects[i] = transform_rect(js2rect(js,val), &cam_mat);
      JS_FreeValue(js,val);
    }
    SDL_RenderRects(r,rects,len);
    return JS_NULL;
  }
  
  rect rect = js2rect(js,argv[0]);
  
  rect = transform_rect(rect, &cam_mat);
  
  SDL_RenderRect(r, &rect);
)

JSC_CCALL(renderer_load_texture,
  SDL_Renderer *r = js2SDL_Renderer(js,self);
  SDL_Surface *surf = js2SDL_Surface(js,argv[0]);
  if (!surf) return JS_ThrowReferenceError(js, "Surface was not a surface.");
  SDL_Texture *tex = SDL_CreateTextureFromSurface(r,surf);
  if (!tex) return JS_ThrowReferenceError(js, "Could not create texture from surface: %s", SDL_GetError());
  ret = SDL_Texture2js(js,tex);
  JS_SetPropertyStr(js,ret,"width", number2js(js,tex->w));
  JS_SetPropertyStr(js,ret,"height", number2js(js,tex->h));
)

JSC_CCALL(SDL_Renderer_fillrect,
  SDL_Renderer *r = js2SDL_Renderer(js,self);
  if (!JS_IsNull(argv[1])) {
    colorf color = js2color(js,argv[1]);  
    SDL_SetRenderDrawColorFloat(r, color.r, color.g, color.b, color.a);
  }

  if (JS_IsArray(js,argv[0])) {
    int len = JS_ArrayLength(js,argv[0]);
    rect rects[len];
    for (int i = 0; i < len; i++) {
      JSValue val = JS_GetPropertyUint32(js,argv[0],i);
      rects[i] = js2rect(js,val);
      JS_FreeValue(js,val);
    }
    if (!SDL_RenderFillRects(r,rects,len))
      return JS_ThrowReferenceError(js, "Could not render rectangle: %s", SDL_GetError());
  }
  rect rect = transform_rect(js2rect(js,argv[0]),&cam_mat);
  
  if (!SDL_RenderFillRect(r, &rect))
    return JS_ThrowReferenceError(js, "Could not render rectangle: %s", SDL_GetError());
)

JSC_CCALL(renderer_texture,
  SDL_Renderer *renderer = js2SDL_Renderer(js,self);
  SDL_Texture *tex = js2SDL_Texture(js,argv[0]);
  rect dst = transform_rect(js2rect(js,argv[1]), &cam_mat);
  
  if (!JS_IsNull(argv[3])) {
    colorf color = js2color(js,argv[3]);
    SDL_SetTextureColorModFloat(tex, color.r, color.g, color.b);
    SDL_SetTextureAlphaModFloat(tex,color.a);
  }
  if (JS_IsNull(argv[2]))
    SDL_RenderTexture(renderer,tex,NULL,&dst);
  else {

    rect src = js2rect(js,argv[2]);

    SDL_RenderTextureRotated(renderer, tex, &src, &dst, 0, NULL, SDL_FLIP_NONE);
  }
)

JSC_CCALL(renderer_tile,
  SDL_Renderer *renderer = js2SDL_Renderer(js,self);
  if (!renderer) return JS_ThrowTypeError(js,"self was not a renderer");
  SDL_Texture *tex = js2SDL_Texture(js,argv[0]);
  if (!tex) return JS_ThrowTypeError(js,"first argument was not a texture");
  rect dst = js2rect(js,argv[1]);
  if (!dst.w) dst.w = tex->w;
  if (!dst.h) dst.h = tex->h;
  float scale = js2number(js,argv[3]);
  if (!scale) scale = 1;
  if (JS_IsNull(argv[2]))
    SDL_RenderTextureTiled(renderer,tex,NULL,scale, &dst);
  else {
    rect src = js2rect(js,argv[2]);
    SDL_RenderTextureTiled(renderer,tex,&src,scale, &dst);
  }
)

JSC_CCALL(renderer_slice9,
  SDL_Renderer *renderer = js2SDL_Renderer(js,self);
  SDL_Texture *tex = js2SDL_Texture(js,argv[0]);
  lrtb bounds = js2lrtb(js,argv[2]);
  rect src, dst;
  src = transform_rect(js2rect(js,argv[3]),&cam_mat);
  dst = transform_rect(js2rect(js,argv[1]), &cam_mat);

  SDL_RenderTexture9Grid(renderer, tex,
    JS_IsNull(argv[3]) ? NULL : &src,
    bounds.l, bounds.r, bounds.t, bounds.b, 0.0,
    JS_IsNull(argv[1]) ? NULL : &dst);
)

JSC_CCALL(renderer_get_image,
  SDL_Renderer *r = js2SDL_Renderer(js,self);
  SDL_Surface *surf = NULL;
  if (!JS_IsNull(argv[0])) {
    rect rect = js2rect(js,argv[0]);
    surf = SDL_RenderReadPixels(r,&rect);
  } else
    surf = SDL_RenderReadPixels(r,NULL);
  if (!surf) return JS_ThrowReferenceError(js, "could not make surface from renderer");
  return SDL_Surface2js(js,surf);
)

JSC_SCALL(renderer_fasttext,
  SDL_Renderer *r = js2SDL_Renderer(js,self);
  if (!JS_IsNull(argv[2])) {
    colorf color = js2color(js,argv[2]);  
    SDL_SetRenderDrawColorFloat(r, color.r, color.g, color.b, color.a);
  }
  HMM_Vec2 pos = js2vec2(js,argv[1]);
  pos.y += 8;
  HMM_Vec2 tpos = HMM_MulM3V3(cam_mat, (HMM_Vec3){pos.x,pos.y,1}).xy;
  SDL_RenderDebugText(r, tpos.x, tpos.y, str);
)

JSC_CCALL(renderer_line,
  SDL_Renderer *r = js2SDL_Renderer(js,self);
  if (!JS_IsNull(argv[1])) {
    colorf color = js2color(js,argv[1]);  
    SDL_SetRenderDrawColorFloat(r, color.r, color.g, color.b, color.a);
  }

  if (JS_IsArray(js,argv[0])) {
    int len = JS_ArrayLength(js,argv[0]);
    HMM_Vec2 points[len];
    assert(sizeof(HMM_Vec2) == sizeof(SDL_FPoint));
    for (int i = 0; i < len; i++) {
      JSValue val = JS_GetPropertyUint32(js,argv[0],i);
      points[i] = js2vec2(js,val);
      JS_FreeValue(js,val);
    }
    SDL_RenderLines(r,points,len);
  }
)

JSC_CCALL(renderer_point,
  SDL_Renderer *r = js2SDL_Renderer(js,self);
  if (!JS_IsNull(argv[1])) {
    colorf color = js2color(js,argv[1]);  
    SDL_SetRenderDrawColorFloat(r, color.r, color.g, color.b, color.a);
  }

  if (JS_IsArray(js,argv[0])) {
    int len = JS_ArrayLength(js,argv[0]);
    HMM_Vec2 points[len];
    assert(sizeof(HMM_Vec2) ==sizeof(SDL_FPoint));
    for (int i = 0; i < len; i++) {
      JSValue val = JS_GetPropertyUint32(js,argv[0],i);
      points[i] = js2vec2(js,val);
      JS_FreeValue(js,val);
    }
    SDL_RenderPoints(r, points, len);
    return JS_NULL;
  }

  HMM_Vec2 point = transform_point(r, js2vec2(js,argv[0]), &cam_mat);
  SDL_RenderPoint(r,point.x,point.y);
)

// Function to translate a list of 2D points
void Translate2DPoints(HMM_Vec2 *points, int count, HMM_Vec3 position, HMM_Quat rotation, HMM_Vec3 scale) {
    // Precompute the 2D rotation matrix from the quaternion
    float xx = rotation.x * rotation.x;
    float yy = rotation.y * rotation.y;
    float zz = rotation.z * rotation.z;
    float xy = rotation.x * rotation.y;
    float zw = rotation.z * rotation.w;

    // Extract 2D affine rotation and scaling
    float m00 = (1.0f - 2.0f * (yy + zz)) * scale.x; // Row 1, Column 1
    float m01 = (2.0f * (xy + zw)) * scale.y;        // Row 1, Column 2
    float m10 = (2.0f * (xy - zw)) * scale.x;        // Row 2, Column 1
    float m11 = (1.0f - 2.0f * (xx + zz)) * scale.y; // Row 2, Column 2

    // Translation components (ignore the z position)
    float tx = position.x;
    float ty = position.y;

    // Transform each point
    for (int i = 0; i < count; ++i) {
        HMM_Vec2 p = points[i];
        points[i].x = m00 * p.x + m01 * p.y + tx;
        points[i].y = m10 * p.x + m11 * p.y + ty;
    }
}

// Should take a single struct with pos, color, uv, and indices arrays
JSC_CCALL(renderer_geometry,
  SDL_Renderer *r = js2SDL_Renderer(js,self);
  JSValue pos = JS_GetPropertyStr(js,argv[1], "pos");
  JSValue color = JS_GetPropertyStr(js,argv[1], "color");
  JSValue uv = JS_GetPropertyStr(js,argv[1], "uv");
  JSValue indices = JS_GetPropertyStr(js,argv[1], "indices");
  int vertices = js_getnum_str(js, argv[1], "vertices");
  int count = js_getnum_str(js, argv[1], "count");

  size_t pos_stride, indices_stride, uv_stride, color_stride;
  void *posdata = get_gpu_buffer(js,pos, &pos_stride, NULL);
  void *idxdata = get_gpu_buffer(js,indices, &indices_stride, NULL);
  void *uvdata = get_gpu_buffer(js,uv, &uv_stride, NULL);
  void *colordata = get_gpu_buffer(js,color,&color_stride, NULL);

  SDL_Texture *tex = js2SDL_Texture(js,argv[0]);

  HMM_Vec2 *trans_pos = malloc(vertices*sizeof(HMM_Vec2));
  memcpy(trans_pos,posdata, sizeof(HMM_Vec2)*vertices);

  for (int i = 0; i < vertices; i++)
    trans_pos[i] = HMM_MulM3V3(cam_mat, (HMM_Vec3){trans_pos[i].x, trans_pos[i].y, 1}).xy;
    
  if (!SDL_RenderGeometryRaw(r, tex, trans_pos, pos_stride,colordata,color_stride,uvdata, uv_stride, vertices, idxdata, count, indices_stride))
    ret = JS_ThrowReferenceError(js, "Error rendering geometry: %s",SDL_GetError());

    free(trans_pos);

  JS_FreeValue(js,pos);
  JS_FreeValue(js,color);
  JS_FreeValue(js,uv);
  JS_FreeValue(js,indices);
)

JSC_CCALL(renderer_logical_size,
  SDL_Renderer *r = js2SDL_Renderer(js,self);
  HMM_Vec2 v = js2vec2(js,argv[0]);
  SDL_SetRenderLogicalPresentation(r,v.x,v.y,SDL_LOGICAL_PRESENTATION_INTEGER_SCALE);
)

JSC_CCALL(renderer_viewport,
  SDL_Renderer *r = js2SDL_Renderer(js,self);
  if (JS_IsNull(argv[0]))
    SDL_SetRenderViewport(r,NULL);
  else {
    rect view = js2rect(js,argv[0]);
    SDL_SetRenderViewport(r,&view);
  }
)

JSC_CCALL(renderer_get_viewport,
  SDL_Renderer *r = js2SDL_Renderer(js,self);  
  SDL_Rect vp;
  SDL_GetRenderViewport(r, &vp);
  rect re;
  re.x = vp.x;
  re.y = vp.y;
  re.h = vp.h;
  re.w = vp.w;
  return rect2js(js,re);
)

JSC_CCALL(renderer_clip,
  SDL_Renderer *r = js2SDL_Renderer(js,self);
  if (JS_IsNull(argv[0]))
    SDL_SetRenderClipRect(r,NULL);
  else {
    rect view = js2rect(js,argv[0]);
    SDL_SetRenderClipRect(r,&view);
  }
)

JSC_CCALL(renderer_scale,
  SDL_Renderer *r = js2SDL_Renderer(js,self);
  HMM_Vec2 v = js2vec2(js,argv[0]);
  SDL_SetRenderScale(r, v.x, v.y);
)

JSC_CCALL(renderer_vsync,
  SDL_Renderer *r = js2SDL_Renderer(js,self);
  SDL_SetRenderVSync(r,js2number(js,argv[0]));
)

// This returns the coordinates inside the 
JSC_CCALL(renderer_coords,
  SDL_Renderer *r = js2SDL_Renderer(js,self);
  HMM_Vec2 pos, coord;
  pos = js2vec2(js,argv[0]);
  SDL_RenderCoordinatesFromWindow(r,pos.x,pos.y, &coord.x, &coord.y);
  return vec22js(js,coord);
)

JSC_CCALL(renderer_camera,
  int centered = JS_ToBool(js,argv[1]);
  SDL_Renderer *ren = js2SDL_Renderer(js,self);
  SDL_Rect vp;
  SDL_GetRenderViewport(ren, &vp);
  HMM_Mat3 proj;
  proj.Columns[0] = (HMM_Vec3){1,0,0};
  proj.Columns[1] = (HMM_Vec3){0,-1,0};
  if (centered)
    proj.Columns[2] = (HMM_Vec3){vp.w/2.0,vp.h/2.0,1};
  else
    proj.Columns[2] = (HMM_Vec3){0,vp.h,1};
    
  transform *tra = js2transform(js,argv[0]);  
  HMM_Mat3 view;
  view.Columns[0] = (HMM_Vec3){1,0,0};
  view.Columns[1] = (HMM_Vec3){0,1,0};
  view.Columns[2] = (HMM_Vec3){-tra->pos.x, -tra->pos.y,1};
  cam_mat = HMM_MulM3(proj,view);
)

JSC_CCALL(renderer_screen2world,
  HMM_Mat3 inv = HMM_InvGeneralM3(cam_mat);
  HMM_Vec3 pos = js2vec3(js,argv[0]);
  return vec22js(js, HMM_MulM3V3(inv, pos).xy);
)

JSC_CCALL(renderer_target,
  SDL_Renderer *r = js2SDL_Renderer(js,self);
  if (JS_IsNull(argv[0]))
    SDL_SetRenderTarget(r, NULL);
  else {
    SDL_Texture *tex = js2SDL_Texture(js,argv[0]);
    SDL_SetRenderTarget(r,tex);
  }
)

// Given an array of sprites, make the necessary geometry
// A sprite is expected to have:
// transform: a transform encoding position and rotation. its scale is in pixels - so a scale of 1 means the image will draw only on a single pixel.
// image: a standard prosperon image of a surface, rect, and texture
// color: the color this sprite should be hued by
JSC_CCALL(renderer_make_sprite_mesh,
  JSValue sprites = argv[0];
  size_t quads = JS_ArrayLength(js,argv[0]);
  size_t verts = quads*4;
  size_t count = quads*6;

  HMM_Vec2 *posdata = malloc(sizeof(*posdata)*verts);
  HMM_Vec2 *uvdata = malloc(sizeof(*uvdata)*verts);
  HMM_Vec4 *colordata = malloc(sizeof(*colordata)*verts);

  for (int i = 0; i < quads; i++) {
    JSValue sub = JS_GetPropertyUint32(js,sprites,i);
    JSValue jstransform = JS_GetPropertyStr(js,sub,"transform");

    JSValue jssrc = JS_GetPropertyStr(js,sub,"src");
    JSValue jscolor = JS_GetPropertyStr(js,sub,"color");
    HMM_Vec4 color;

    rect src;
    if (JS_IsNull(jssrc))
      src = (rect){.x = 0, .y = 0, .w = 1, .h = 1};
    else
      src = js2rect(js,jssrc);
    
    if (JS_IsNull(jscolor))
      color = (HMM_Vec4){1,1,1,1};
    else
      color = js2vec4(js,jscolor);

    // Calculate the base index for the current quad
    size_t base = i * 4;

    // Define the UV coordinates based on the source rectangle
    uvdata[base + 0] = (HMM_Vec2){ src.x,            src.y + src.h };
    uvdata[base + 1] = (HMM_Vec2){ src.x + src.w,    src.y + src.h };    
    uvdata[base + 2] = (HMM_Vec2){ src.x,            src.y };
    uvdata[base + 3] = (HMM_Vec2){ src.x + src.w,    src.y };

    colordata[base] = color;
    colordata[base+1] = color;
    colordata[base+2] = color;
    colordata[base+3] = color;
    
    JS_FreeValue(js,jstransform);
    JS_FreeValue(js,sub);
    JS_FreeValue(js,jscolor);
    JS_FreeValue(js,jssrc);
  }

  ret = JS_NewObject(js);
  JS_SetPropertyStr(js, ret, "pos", make_gpu_buffer(js, posdata, sizeof(*posdata) * verts, JS_TYPED_ARRAY_FLOAT32, 2, 0,0));
  JS_SetPropertyStr(js, ret, "uv", make_gpu_buffer(js, uvdata, sizeof(*uvdata) * verts, JS_TYPED_ARRAY_FLOAT32, 2, 0,0));
  JS_SetPropertyStr(js, ret, "color", make_gpu_buffer(js, colordata, sizeof(*colordata) * verts, JS_TYPED_ARRAY_FLOAT32, 4, 0,0));
  JS_SetPropertyStr(js, ret, "indices", make_quad_indices_buffer(js, quads));
  JS_SetPropertyStr(js, ret, "vertices", number2js(js, verts));
  JS_SetPropertyStr(js, ret, "count", number2js(js, count));
)

static const JSCFunctionListEntry js_renderer_funcs[] = {
  JS_CFUNC_DEF("clear",           0, js_renderer_clear),
  JS_CFUNC_DEF("present",         0, js_renderer_present),
  JS_CFUNC_DEF("draw_color",      1, js_renderer_draw_color),
  JS_CFUNC_DEF("rect",            2, js_renderer_rect),
  JS_CFUNC_DEF("fillrect",        2, js_renderer_fillrect),
  JS_CFUNC_DEF("line",            2, js_renderer_line),
  JS_CFUNC_DEF("point",           2, js_renderer_point),
  JS_CFUNC_DEF("load_texture",    1, js_renderer_load_texture),
  JS_CFUNC_DEF("texture",         4, js_renderer_texture),
  JS_CFUNC_DEF("slice9",          4, js_renderer_slice9),
  JS_CFUNC_DEF("tile",            4, js_renderer_tile),
  JS_CFUNC_DEF("get_image",       1, js_renderer_get_image),
  JS_CFUNC_DEF("fasttext",        2, js_renderer_fasttext),
  JS_CFUNC_DEF("geometry",        2, js_renderer_geometry),
  JS_CFUNC_DEF("scale",           1, js_renderer_scale),
  JS_CFUNC_DEF("logical_size",    1, js_renderer_logical_size),
  JS_CFUNC_DEF("viewport",        1, js_renderer_viewport),
  JS_CFUNC_DEF("clip",            1, js_renderer_clip),
  JS_CFUNC_DEF("vsync",           1, js_renderer_vsync),
  JS_CFUNC_DEF("coords",          1, js_renderer_coords),
  JS_CFUNC_DEF("camera",          2, js_renderer_camera),
  JS_CFUNC_DEF("get_viewport",    0, js_renderer_get_viewport),
  JS_CFUNC_DEF("screen2world",    1, js_renderer_screen2world),
  JS_CFUNC_DEF("target",          1, js_renderer_target),
  JS_CFUNC_DEF("make_sprite_mesh",2, js_renderer_make_sprite_mesh),
};

JSC_CCALL(mod_create,
  SDL_Window *win = js2SDL_Window(js,self);
  SDL_PropertiesID props = SDL_CreateProperties();
  SDL_SetNumberProperty(props, SDL_PROP_RENDERER_CREATE_PRESENT_VSYNC_NUMBER, 0);
  SDL_SetPointerProperty(props, SDL_PROP_RENDERER_CREATE_WINDOW_POINTER, win);
  SDL_SetStringProperty(props, SDL_PROP_RENDERER_CREATE_NAME_STRING, str);
  SDL_Renderer *r = SDL_CreateRendererWithProperties(props);
  SDL_DestroyProperties(props);
  if (!r) return JS_ThrowReferenceError(js, "Error creating renderer: %s",SDL_GetError());
  SDL_SetRenderDrawBlendMode(r, SDL_BLENDMODE_BLEND);
  return SDL_Renderer2js(js,r);
)

static const JSCFunctionListEntry js_mod_funcs[] = {
  JS_CFUNC_DEF("create", 1, js_mod_create)
};

JSValue js_renderer_use(JSContext *ctx) {
  // Create an object that will hold all the "renderer" methods
  JSValue obj = JS_NewObject(ctx);

  // Add all the above C functions as properties of that object
  JS_SetPropertyFunctionList(ctx, obj,
                             js_renderer_funcs,
                             sizeof(js_renderer_funcs)/sizeof(JSCFunctionListEntry));
  return obj;
}
