var graphics = this

graphics[cell.DOC] = `
Provides functionality for loading and managing images, fonts, textures, and sprite meshes. 
Includes both JavaScript and C-implemented routines for creating geometry buffers, performing 
rectangle packing, etc.
`

var renderer_actor = arg[0]

var io = use('io')
var time = use('time')
var res = use('resources')
var json = use('json')

var GPU = Symbol()
var CPU = Symbol()
var LASTUSE = Symbol()
var LOADING = Symbol()

var cache = {}

// Image constructor function
graphics.Image = function(surfaceData) {
  // Initialize private properties
  this[CPU] = surfaceData || null;
  this[GPU] = null;
  this[LOADING] = false;
  this[LASTUSE] = time.number();
  this.rect = {x:0, y:0, width:surfaceData.width, height:surfaceData.height};
}

// Define getters and methods on the prototype
Object.defineProperties(graphics.Image.prototype, {
  gpu: {
    get: function() {
      if (!this[GPU] && !this[LOADING]) {
        this[LOADING] = true;
        var self = this;
        
        // Send message to load texture
        send(renderer_actor, {
          kind: "renderer",
          op: "loadTexture",
          data: this[CPU]
        }, function(response) {
          if (response.error) {
            log.error("Failed to load texture:")
            log.error(response.error)
            self[LOADING] = false;
          } else {
            self[GPU] = response;
            decorate_rect_px(self);
            self[LOADING] = false;
          }
        });
      }
      
      return this[GPU]
    }
  },
  
  texture: {
    get: function() { return this.gpu }
  },

  cpu: {
    get: function() {
      return this[CPU]
    }
  },
  
  surface: {
    get: function() { return this.cpu }
  },
  
  width: {
    get: function() {
      return this[CPU].width
    }
  },
  
  height: {
    get: function() {
      return this[CPU].height
    }
  }
});

// Add methods to prototype
graphics.Image.prototype.unload_gpu = function() {
  this[GPU] = null
}

graphics.Image.prototype.unload_cpu = function() {
  this[CPU] = null
}

function calc_image_size(img) {
  if (!img.texture || !img.rect) return
  return [img.texture.width * img.rect.width, img.texture.height * img.rect.height]
}

function decorate_rect_px(img) {
  // needs a GPU texture to measure
  if (!img || !img.texture) return

  // default UV rect is the whole image if none supplied
  img.rect ??= {x:0, y:0, width:1, height:1}                // [u0,v0,uw,vh]  in 0-1

  // store pixel-space version: [x, y, w, h]  in texels
  img.rect_px = {
    x:Math.round(img.rect.x * img.texture.width),
    y:Math.round(img.rect.y * img.texture.height),
    width:Math.round(img.rect.width * img.texture.width),
    height:Math.round(img.rect.height * img.texture.height)
  }
}

function make_handle(obj)
{
  return new graphics.Image(obj);
}

function wrapSurface(surf, maybeRect){
  def h = make_handle(surf);
  if(maybeRect) h.rect = maybeRect;  /* honour frame sub-rect */
  return h;
}
function wrapFrames(arr){            /* [{surface,time,rect}, …] → [{image,time}] */
  return arr.map(f => ({
    image : wrapSurface(f.surface || f),   /* accept bare surface too */
    time: f.time,
    rect: f.rect                         /* keep for reference */
  }));
}
function makeAnim(frames, loop=true){
  return { frames, loop }
}

function decode_image(bytes, ext)
{
  switch(ext) {
    case 'gif': return graphics.make_gif(bytes)
    case 'ase':
    case 'aseprite': return graphics.make_aseprite(bytes)
    default: return {surface:graphics.make_texture(bytes)}
  }
}

function create_image(path){
  try{
    def bytes = io.slurpbytes(path);

    let raw     = decode_image(bytes, path.ext());
    
    /* ── Case A: static image ─────────────────────────────────── */
    if(raw.surface) {
      var gg = new graphics.Image(raw.surface)
      return gg
    }

    /* ── Case B: GIF helpers returned array [surf, …] ─────────── */
    if(Array.isArray(raw))
      return makeAnim( wrapFrames(raw), true );

    /* ── Case C: GIF helpers returned {frames,loop} ───────────── */
    if(raw.frames && Array.isArray(raw.frames))
      return makeAnim( wrapFrames(raw.frames), !!raw.loop );

    /* ── Case D: ASE helpers returned { animName:{frames,loop}, … } ── */
    def anims = {};
    for(def [name, anim] of Object.entries(raw)){
      if(anim && Array.isArray(anim.frames))
        anims[name] = makeAnim( wrapFrames(anim.frames), !!anim.loop );
      else if(anim && anim.surface)          /* ase with flat surface */
        anims[name] = makeAnim(
          [{image:make_handle(anim.surface),time:0}], true );
    }
    if(Object.keys(anims).length) return anims;

    throw new Error('Unsupported image structure from decoder');

  }catch(e){
    log.error(`Error loading image ${path}: ${e.message}`);
    throw e;
  }
}

var image = {}
image.dimensions = function() {
  return [this.texture.width, this.texture.height].scale([this.rect[2], this.rect[3]])
}
image.dimensions[cell.DOC] = `
:return: A 2D array [width, height] that is the scaled size of this image (texture size * rect size).
`

var spritesheet
var sheet_frames = []
var sheetsize = 1024

/**
Pack multiple images into a single texture sheet for efficiency. 
Currently unimplemented (returns immediately).
*/
function pack_into_sheet(images) {
  return
  // This code is currently disabled with an immediate return.
  // Implementation details commented out below.
}

graphics.is_image = function(obj) {
  if (obj.texture && obj.rect) return true
}
graphics.is_image[cell.DOC] = `
:param obj: An object to check.
:return: True if 'obj' has a .texture and a .rect property, indicating it's an image object.
`

graphics.texture_from_data = function(data)
{
  if (!(data instanceof ArrayBuffer)) return null
  
  var image = graphics.make_texture(data);
  var img = make_handle(image)

  img.gpu;
  
  return img;
}

graphics.from_surface = function(id, surf)
{
  return make_handle(surf)
}

graphics.from = function(id, data)
{
  if (typeof id != 'string')
    throw new Error('Expected a string ID')
    
  if (data instanceof ArrayBuffer)
  return graphics.texture_from_data(data) 
}

graphics.texture = function texture(path) {
  if (path instanceof graphics.Image) return path
  
  if (typeof path != 'string')
    throw new Error('need a string for graphics.texture')

  var id = path //.split(':')[0]
  if (cache[id]) return cache[id]
  
  var ipath = res.find_image(id)
  if (!ipath)
    throw new Error(`unknown image ${id}`)
  
  var image = create_image(ipath)
  cache[id] = image
  return image
}
graphics.texture[cell.DOC] = `
:param path: A string path to an image file or an already-loaded image object.
:return: An image object with {surface, texture, frames?, etc.} depending on the format.
Load or retrieve a cached image, converting it into a GPU texture. If 'path' is already an object, it’s returned directly.
`

graphics.texture.cache = {}
graphics.texture.time_cache = {}

graphics.texture.total_size = function() {
  var size = 0
  // Not yet implemented, presumably sum of (texture.width * texture.height * 4) for images in RAM
  return size
}
graphics.texture.total_size[cell.DOC] = `
:return: The total estimated memory size of all cached textures in RAM, in bytes. (Not yet implemented.)
`

graphics.texture.total_vram = function() {
  var vram = 0
  // Not yet implemented, presumably sum of GPU memory usage
  return vram
}
graphics.texture.total_vram[cell.DOC] = `
:return: The total estimated GPU memory usage of all cached textures, in bytes. (Not yet implemented.)
`

graphics.tex_hotreload = function tex_hotreload(file) {
  log.console(`hot reloading ${file}`)
  if (!(file in graphics.texture.cache)) return
  log.console('really doing it')

  var img = create_image(file)
  var oldimg = graphics.texture.cache[file]
  log.console(`new image:${json.encode(img)}`)
  log.console(`old image: ${json.encode(oldimg)}`)

  merge_objects(oldimg, img, ['surface', 'texture', 'loop', 'time'])
}
graphics.tex_hotreload[cell.DOC] = `
:param file: The file path that was changed on disk.
:return: None
Reload the image for the given file, updating the cached copy in memory and GPU. 
`

/**
Merges specific properties from nv into ov, using an array of property names.
*/
function merge_objects(ov, nv, arr) {
  arr.forEach(x => ov[x] = nv[x])
}

/**
Unimplemented function for creating a spritesheet out of multiple images.
*/
function make_spritesheet(paths, width, height) {
  return
}

/**
Stores previously loaded fonts. Keyed by e.g. "path.ttf.16" -> fontObject.
*/
var fontcache = {}
var datas = []

graphics.get_font = function get_font(path, size) {
  var parts = path.split('.')
  if (!isNaN(parts[1])) {
    path = parts[0]
    size = Number(parts[1])
  }
  var fullpath = res.find_font(path)
  if (!fullpath) throw new Error(`Cannot load font ${path}`)

  var fontstr = `${fullpath}.${size}`
  if (fontcache[fontstr]) return fontcache[fontstr]

  var data = io.slurpbytes(fullpath)
  var font = graphics.make_font(data,size)
  
  // Load font texture via renderer actor (async)
  send(renderer_actor, {
    kind: "renderer",
    op: "loadTexture",
    data: font.surface
  }, function(response) {
    if (response.error) {
      log.error("Failed to load font texture:", response.error);
    } else {
      font.texture = response;
    }
  });
  
  fontcache[fontstr] = font

  return font
}
graphics.get_font[cell.DOC] = `
:param path: A string path to a font file, optionally with ".size" appended.
:param size: Pixel size of the font, if not included in 'path'.
:return: A font object with .surface and .texture for rendering text.
Load a font from file if not cached, or retrieve from cache if already loaded.
`

graphics.queue_sprite_mesh = function(queue) {
  var sprites = queue.filter(x => x.type == 'sprite')
  if (sprites.length == 0) return []
  var mesh = graphics.make_sprite_mesh(sprites)
  for (var i = 0; i < sprites.length; i++) {
    sprites[i].mesh = mesh
    sprites[i].first_index = i*6
    sprites[i].num_indices = 6
  }
  return [mesh.pos, mesh.uv, mesh.color, mesh.indices]
}
graphics.queue_sprite_mesh[cell.DOC] = `
:param queue: An array of draw commands, some of which are {type:'sprite'} objects.
:return: An array of references to GPU buffers [pos,uv,color,indices].
Builds a single geometry mesh for all sprite-type commands in the queue, storing first_index/num_indices 
so they can be rendered in one draw call.
`

graphics.make_text_buffer[cell.DOC] = `
:param text: The string to render.
:param rect: A rectangle specifying position and possibly wrapping.
:param angle: Rotation angle (unused or optional).
:param color: A color for the text (could be a vec4).
:param wrap: The width in pixels to wrap text, or 0 for no wrap.
:param font: A font object created by graphics.make_font or graphics.get_font.
:return: A geometry buffer mesh (pos, uv, color, indices) for rendering text.
Generate a GPU buffer mesh of text quads for rendering with a font, etc.
`

graphics.rectpack[cell.DOC] = `
:param width: The width of the area to pack into.
:param height: The height of the area to pack into.
:param sizes: An array of [w,h] pairs for the rectangles to pack.
:return: An array of [x,y] coordinates placing each rect, or null if they don't fit.
Perform a rectangle packing using the stbrp library. Return positions for each rect.
`

graphics.make_texture[cell.DOC] = `
:param data: Raw image bytes (PNG, JPG, etc.) as an ArrayBuffer.
:return: An SDL_Surface object representing the decoded image in RAM, for use with GPU or software rendering.
Convert raw image bytes into an SDL_Surface object.
`

graphics.make_gif[cell.DOC] = `
:param data: An ArrayBuffer containing GIF data.
:return: An object with frames[], each frame having its own .surface. Some also have a .texture for GPU use.
Load a GIF, returning its frames. If it's a single-frame GIF, the result may have .surface only.
`

graphics.make_aseprite[cell.DOC] = `
:param data: An ArrayBuffer containing Aseprite (ASE) file data.
:return: An object containing frames or animations, each with .surface. May also have top-level .surface for a single-layer case.
Load an Aseprite/ASE file from an array of bytes, returning frames or animations.
`

graphics.cull_sprites[cell.DOC] = `
:param sprites: An array of sprite objects (each has rect or transform).
:param camera: A camera or bounding rectangle defining the view area.
:return: A new array of sprites that are visible in the camera's view.
Filter an array of sprites to only those visible in the provided camera’s view.
`

graphics.make_font[cell.DOC] = `
:param data: TTF/OTF file data as an ArrayBuffer.
:param size: Pixel size for rendering glyphs.
:return: A font object with surface, texture, and glyph data, for text rendering with make_text_buffer.
Load a font from TTF/OTF data at the given size.
`

graphics.make_line_prim[cell.DOC] = `
:param points: An array of [x,y] points forming the line.
:param thickness: The thickness (width) of the polyline.
:param startCap: (Unused) Possibly the type of cap for the start.
:param endCap: (Unused) Possibly the type of cap for the end.
:param color: A color to apply to the line.
:return: A geometry mesh object suitable for rendering the line via a pipeline command.
Build a GPU mesh representing a thick polyline from an array of points, using parsl or a similar library under the hood.
`

return graphics
