var graphics = {}

var io = use('cellfs')
var time = use('time')
var res = use('resources')
var json = use('json')
var os = use('os')
var staef = use('staef')
var qoi = use('image/qoi')
var png = use('image/png')
var gif = use('image/gif')
var aseprite = use('image/aseprite')

var LASTUSE = "graphics:lastuse"
var LOADING = "graphics:loading"

var cache = {}

// cpu is the surface
graphics.Image = function(surfaceData) {
  var img = {}
  img.cpu = surfaceData || null;
  img.texture = 0;
  img.surface = img.cpu;
  img.width = surfaceData?.width || 0;
  img.height = surfaceData?.height || 0;
  img.rect = {x:0, y:0, width:img.width, height:img.height};
  img[LOADING] = false;
  img[LASTUSE] = time.number();
  return img
}

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

function decorate_rect_px(img) {
  // 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

  var width = 0, height = 0;
  if (img.texture) {
    width = img.texture.width;
    height = img.texture.height;
  } else if (img.cpu) {
    width = img.cpu.width;
    height = img.cpu.height;
  } else {
    return;
  }

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

function make_handle(obj)
{
  var img = graphics.Image(obj);
  decorate_rect_px(img);
  return img;
}

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 array(arr, f => {
    // Handle both surface objects and objects with surface property
    var surf = f.surface || f;
    return {
      image: wrapSurface(surf),
      time: f.time || 0,
      rect: f.rect                         /* keep for reference */
    }
  });
}
function makeAnim(frames, loop=true){
  return { frames, loop }
}

function decode_image(bytes, ext)
{
  switch(ext) {
    case 'gif': return decode_gif(gif.decode(bytes))
    case 'ase':
    case 'aseprite': return decode_aseprite(aseprite.decode(bytes))
    case 'qoi': return qoi.decode(bytes)  // returns single surface
    case 'png': return png.decode(bytes)  // returns single surface
    case 'jpg':
    case 'jpeg': return png.decode(bytes)  // png.decode handles jpg too via stb_image
    case 'bmp': return png.decode(bytes)  // png.decode handles bmp too via stb_image
    default: 
      // Try QOI first since it's fast to check
      var qoi_result = qoi.decode(bytes)
      if (qoi_result) return qoi_result
      // Fall back to png decoder for other formats (uses stb_image)
      return png.decode(bytes)
  }
}

// Convert gif.decode output to graphics.cm format
function decode_gif(decoded) {
  if (!decoded || !decoded.frames) return null
  
  // Single frame - return just the surface
  if (decoded.frame_count == 1) {
    return decoded.frames[0]
  }
  
  // Multiple frames - return array with time property
  return array(decoded.frames, frame => {
    return {
      surface: frame,
      time: (frame.duration || 100) / 1000.0  // convert ms to seconds
    }
  })
}

// Convert aseprite.decode output to graphics.cm format
function decode_aseprite(decoded) {
  if (!decoded) return null
  
  // Single frame - return just the surface
  if (decoded.frame_count == 1) {
    return { surface: decoded.frames[0] }
  }
  
  // Multiple frames without tags - return as single animation
  return {
    frames: array(decoded.frames, frame => {
      return {
        surface: frame,
        time: (frame.duration || 100) / 1000.0  // convert ms to seconds
      }
    }),
    loop: true
  }
}

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

    var ext = pop(array(path, '.'))
    var raw = decode_image(bytes, ext);
    
    /* ── Case A: single surface (from make_texture) ────────────── */
    if(raw && raw.width && raw.pixels && !is_array(raw)) {
      return graphics.Image(raw)
    }

    /* ── Case B: array of surfaces (from make_gif) ────────────── */
    if(is_array(raw)) {
      // Single frame GIF returns array with one surface
      if(length(raw) == 1 && !raw[0].time) {
        return graphics.Image(raw[0])
      }
      // Multiple frames - create animation
      return makeAnim(wrapFrames(raw), true);
    }

    if(is_object(raw) && !raw.width) {
      if(raw.surface)
        return graphics.Image(raw.surface)
      
      if(raw.frames && is_array(raw.frames) && raw.loop != null)
        return makeAnim(wrapFrames(raw.frames), !!raw.loop);
      
      def anims = {};
      var keys = array(raw)
      arrfor(keys, function(name) {
        var anim = raw[name]
        if(anim && is_array(anim.frames))
          anims[name] = makeAnim(wrapFrames(anim.frames), !!anim.loop);
        else if(anim && anim.surface)
          anims[name] = graphics.Image(anim.surface);
      })
      if(length(array(anims))) return anims;
    }

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

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

var image = {}
image.dimensions = function() {
  var width = 0, height = 0;
  if (this.texture) {
    width = this.texture.width;
    height = this.texture.height;
  } else if (this.cpu) {
    width = this.cpu.width;
    height = this.cpu.height;
  }
  return [width, height].scale([this.rect[2], this.rect[3]])
}

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.texture_from_data = function(data)
{
  if (!is_blob(data)) return null
  
  var image = graphics.make_texture(data);
  var img = make_handle(image)

  if (renderer_actor) img.gpu;
  
  return img;
}

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

graphics.from = function(id, data)
{
  if (!is_text(id))
    throw Error('Expected a string ID')
    
  if (is_blob(data))
    return graphics.texture_from_data(data) 
}

graphics.texture = function texture(path) {
  if (is_proto(path, graphics.Image)) return path
  
  if (!is_text(path))
    throw Error('need a string for graphics.texture')

  var parts = array(path, ':')
  var id = parts[0]
  var animName = parts[1]
  var frameIndex = parts[2]
  
  // Handle the case where animName is actually a frame index (e.g., "gears:0")
  if (animName != null && frameIndex == null && !is_null(number(animName))) {
    frameIndex = number(animName)
    animName = null
  }
  
  if (!cache[id]) {
    var ipath = res.find_image(id)
    
    if (!ipath) {
      // If still not found, return notex
      return graphics.texture('notex')
    }
    
    var result = create_image(ipath)
    cache[id] = result
  }
  
  var cached = cache[id]
  
  // No further path specifiers and no frame index - return the whole thing
  if (!animName && frameIndex == null) return cached
  
  // Handle frame index without animation name (e.g., "gears:0")
  if (!animName && frameIndex != null) {
    // If cached is a single animation (has .frames property)
    if (cached.frames && is_array(cached.frames)) {
      var idx = number(frameIndex)
      if (idx == null) return cached
      // Wrap the index
      idx = idx % length(cached.frames)
      return cached.frames[idx].image
    }
    // If cached is a single Image, any frame index just returns the image
    if (is_proto(cached, graphics.Image)) {
      return cached
    }
  }
  
  // If cached is a single Image, treat it as a single-frame animation
  if (is_proto(cached, graphics.Image)) {
    if (frameIndex != null) {
      // For single images, any frame index just returns the image
      return cached
    }
    // animName without frameIndex for single image - return as single-frame array
    return [cached]
  }
  
  // If cached is a single animation (has .frames property)
  if (cached.frames && is_array(cached.frames)) {
    if (frameIndex != null) {
      var idx = number(frameIndex)
      if (isNaN(idx)) return cached
      // Wrap the index
      idx = idx % length(cached.frames)
      return cached.frames[idx].image
    }
    // Just animation name for single animation - return the animation
    return cached
  }
  
  // If cached is an object of multiple animations
  if (is_object(cached) && !cached.frames) {
    var anim = cached[animName]
    if (!anim) 
      throw Error(`animation ${animName} not found in ${id}`)
    
    if (frameIndex != null) {
      var idx = number(frameIndex)
      if (isNaN(idx)) return anim
      
      if (is_proto(anim, graphics.Image)) {
        // Single image animation - any frame index returns the image
        return anim
      } else if (anim.frames && is_array(anim.frames)) {
        // Multi-frame animation - wrap the index
        idx = idx % length(anim.frames)
        return anim.frames[idx].image
      }
    }
    
    // Just animation name - return the animation
    return anim
  }
  
  return cached
}

graphics.tex_hotreload = function tex_hotreload(file) {
  var basename = array(pop(array(file, '/')), '.')[0]
  
  // Check if this basename exists in our cache
  if (!(basename in cache)) return
  
  // Find the full path for this image
  var fullpath = res.find_image(basename)
  if (!fullpath) return
  
  var img = create_image(fullpath)
  var oldimg = cache[basename]
  
  // Preserve the GPU texture ID if it exists
  var oldGPU = oldimg.gpu
  
  // Update the CPU surface data
  oldimg.cpu = img.cpu
  oldimg.surface = img.cpu
  
  // Clear GPU texture to force reload
  oldimg.gpu = 0
  oldimg.texture = 0
  oldimg[LOADING] = false
  
  // Update dimensions
  if (img.cpu) {
    oldimg.width = img.cpu.width
    oldimg.height = img.cpu.height
    oldimg.rect = {x:0, y:0, width:img.cpu.width, height:img.cpu.height}
    decorate_rect_px(oldimg)
  }
}

/**
Merges specific properties from nv into ov, using an array of property names.
*/
function merge_objects(ov, nv, arr) {
  arrfor(arr, 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) {
  if (is_object(path)) return path
  if (!is_text(path))
    throw Error(`Can't find font with path: ${path}`)

  var parts = array(path, '.')
  var size = 16 // default size
  parts[1] = number(parts[1])
  if (parts[1]) {
    path = parts[0]
    size = parts[1]
  }

  var fullpath = res.find_font(path)
  if (!fullpath) throw Error(`Cannot load font ${path}`)

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

  var data = io.slurp(fullpath)
  var font = staef.font(data, size)
  
  fontcache[fontstr] = font
  
  return font
}

graphics.queue_sprite_mesh = function(queue) {
  var sprites = filter(queue, x => x.type == 'sprite')
  if (length(sprites) == 0) return []
  var mesh = graphics.make_sprite_mesh(sprites)
  for (var i = 0; i < length(sprites); 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]
}

return graphics
