var film2d = {}

var next_id = 1
var registry = {}       // id -> drawable
var group_index = {}    // group_name -> [id, id, ...]
var plane_index = {}    // plane_name -> [id, id, ...]

film2d.register = function(drawable) {
  var id = text(next_id++)
  drawable._id = id
  registry[id] = drawable
  
  // Index by plane
  var plane = drawable.plane || 'default'
  if (!plane_index[plane]) plane_index[plane] = []
  plane_index[plane].push(id)
  
  // Index by groups (effect routing only)
  var groups = drawable.groups || []
  for (var i = 0; i < groups.length; i++) {
    var g = groups[i]
    if (!group_index[g]) group_index[g] = []
    group_index[g].push(id)
  }
  
  return id
}

film2d.unregister = function(id) {
  var id_str = text(id)
  var drawable = registry[id_str]
  if (!drawable) return
  
  // Remove from plane index
  var plane = drawable.plane || 'default'
  if (plane_index[plane]) {
    var idx = plane_index[plane].indexOf(id_str)
    if (idx >= 0) plane_index[plane].splice(idx, 1)
  }
  
  // Remove from group indices
  var groups = drawable.groups || []
  for (var i = 0; i < groups.length; i++) {
    var g = groups[i]
    if (group_index[g]) {
      var idx = group_index[g].indexOf(id_str)
      if (idx >= 0) group_index[g].splice(idx, 1)
    }
  }
  
  delete registry[id_str]
}

film2d.index_group = function(id, group) {
  if (!group_index[group]) group_index[group] = []
  if (group_index[group].indexOf(text(id)) < 0)
    group_index[group].push(text(id))
}

film2d.unindex_group = function(id, group) {
  if (!group_index[group]) return
  var idx = group_index[group].indexOf(text(id))
  if (idx >= 0) group_index[group].splice(idx, 1)
}

film2d.reindex = function(id, old_groups, new_groups) {
  for (var i = 0; i < old_groups.length; i++)
    film2d.unindex_group(id, old_groups[i])
  for (var i = 0; i < new_groups.length; i++)
    film2d.index_group(id, new_groups[i])
}

film2d.get = function(id) {
  return registry[text(id)]
}

// Query by plane and/or group - returns array of drawables
film2d.query = function(selector) {
  var result = []
  
  // Query by plane (primary selection)
  if (selector.plane) {
    var ids = plane_index[selector.plane] || []
    for (var i = 0; i < ids.length; i++) {
      var d = registry[ids[i]]
      if (d && d.visible != false) {
        // If also filtering by group, check membership
        if (selector.group) {
          var groups = d.groups || []
          if (groups.indexOf(selector.group) >= 0) result.push(d)
        } else {
          result.push(d)
        }
      }
    }
    return result
  }
  
  // Query by group only (for effect routing)
  if (selector.group) {
    var ids = group_index[selector.group] || []
    for (var i = 0; i < ids.length; i++) {
      var d = registry[ids[i]]
      if (d && d.visible != false) result.push(d)
    }
    return result
  }
  
  if (selector.groups) {
    var seen = {}
    for (var g = 0; g < selector.groups.length; g++) {
      var ids = group_index[selector.groups[g]] || []
      for (var i = 0; i < ids.length; i++) {
        if (!seen[ids[i]]) {
          seen[ids[i]] = true
          var d = registry[ids[i]]
          if (d && d.visible != false) result.push(d)
        }
      }
    }
    return result
  }
  
  // All drawables
  for (var id in registry) {
    var d = registry[id]
    if (d && d.visible != false) result.push(d)
  }
  return result
}

// Get all groups a drawable belongs to
film2d.get_groups = function(id) {
  var d = registry[text(id)]
  return d ? (d.groups || []) : []
}

// List all known groups
film2d.all_groups = function() {
  var groups = []
  for (var g in group_index)
    if (group_index[g].length > 0) groups.push(g)
  return groups
}

// Render function - takes drawables directly, no tree traversal
film2d.render = function(params, backend) {
  var drawables = params.drawables || []
  var camera = params.camera
  var target = params.target
  var target_size = params.target_size
  var clear_color = params.clear
  var layer_sort = params.layer_sort || {}  // layer -> 'y' or 'explicit'
  
  if (drawables.length == 0) return {commands: []}

    function _y_sort_key(d) {
    if (!d || !d.pos) return 0
    var y = d.pos.y || 0
    var h = d.height || 0
    var ay = d.anchor_y
    if (ay == null) ay = 0.5
    // Convert "pos.y at anchor" -> "feet y"
    return y + h * (1 - ay)
  }
  
  // Sort by layer, then optionally by Y based on layer_sort policy
  drawables.sort(function(a, b) {
    var al = a.layer || 0
    var bl = b.layer || 0
    var dl = al - bl
    if (dl != 0) return dl

    var sort_mode = layer_sort[text(al)] || 'explicit'
    if (sort_mode == 'y') {
      var ay = _y_sort_key(a)
      var by = _y_sort_key(b)

      // Make this explicit instead of guessing
      var y_down = camera && camera.y_down == true

      // If y_down: bigger y is lower on screen => should draw later (on top)
      // If y_up: smaller y is lower on screen => should draw later (on top)
      if (ay != by) return y_down ? (ay - by) : (by - ay)
    }

    var aid = a._id || 0
    var bid = b._id || 0
    return aid < bid ? -1 : 1
  })
  
  var commands = []
  commands.push({cmd: 'begin_render', target: target, clear: clear_color})
  commands.push({cmd: 'set_camera', camera: camera})
  
  var batches = _batch_drawables(drawables)
  
  for (var i = 0; i < batches.length; i++) {
    var batch = batches[i]
    if (batch.type == 'sprite_batch')
      commands.push({cmd: 'draw_batch', batch_type: 'sprites', geometry: {sprites: batch.sprites}, texture: batch.texture, material: batch.material})
    else if (batch.type == 'text')
      commands.push({cmd: 'draw_text', drawable: batch.drawable})
    else if (batch.type == 'texture_ref')
      commands.push({cmd: 'draw_texture_ref', drawable: batch.drawable})
  }
  
  commands.push({cmd: 'end_render'})
  return {commands: commands}
}

function _batch_drawables(drawables) {
  var batches = []
  var current = null
  var default_mat = {blend: 'alpha', sampler: 'nearest'}
  
  for (var i = 0; i < drawables.length; i++) {
    var d = drawables[i]
    
    if (d.type == 'sprite') {
      var tex = d.texture || d.image
      var mat = d.material || {blend: 'alpha', sampler: d.filter || 'nearest'}
      
      if (current && current.type == 'sprite_batch' && current.texture == tex && _mat_eq(current.material, mat)) {
        current.sprites.push(d)
      } else {
        if (current) batches.push(current)
        current = {type: 'sprite_batch', texture: tex, material: mat, sprites: [d]}
      }
    } else if (d.type == 'particles') {
      // Convert particles to sprites
      var tex = d.texture || d.image
      var mat = d.material || default_mat
      var particles = d.particles || []
      var emitter_opacity = d.opacity != null ? d.opacity : 1
      var emitter_tint = d.tint || {r: 1, g: 1, b: 1, a: 1}
      
      for (var p = 0; p < particles.length; p++) {
        var part = particles[p]
        var pc = part.color || {r: 1, g: 1, b: 1, a: 1}
        var sprite = {
          type: 'sprite',
          pos: part.pos,
          width: (d.width || 16) * (part.scale || 1),
          height: (d.height || 16) * (part.scale || 1),
          anchor_x: 0.5,
          anchor_y: 0.5,
          color: pc,
          opacity: emitter_opacity,
          tint: emitter_tint
        }
        
        if (current && current.type == 'sprite_batch' && current.texture == tex && _mat_eq(current.material, mat)) {
          current.sprites.push(sprite)
        } else {
          if (current) batches.push(current)
          current = {type: 'sprite_batch', texture: tex, material: mat, sprites: [sprite]}
        }
      }
    } else if (d.type == 'tilemap') {
      // Expand tilemap to sprites
      var tiles = d.tiles || []
      var tile_w = d.tile_width || 1
      var tile_h = d.tile_height || 1
      var off_x = d.offset_x || 0
      var off_y = d.offset_y || 0
      var tilemap_opacity = d.opacity != null ? d.opacity : 1
      var tilemap_tint = d.tint || {r: 1, g: 1, b: 1, a: 1}
      
      for (var x = 0; x < tiles.length; x++) {
        if (!tiles[x]) continue
        for (var y = 0; y < tiles[x].length; y++) {
          var img = tiles[x][y]
          if (!img) continue
          
          var wx = (x + off_x) * tile_w
          var wy = (y + off_y) * tile_h
          
          // Center anchor for sprite
          var sprite = {
            type: 'sprite',
            image: img,
            pos: {x: wx + tile_w/2, y: wy + tile_h/2},
            width: tile_w,
            height: tile_h,
            anchor_x: 0.5,
            anchor_y: 0.5,
            color: {r: 1, g: 1, b: 1, a: 1},
            opacity: tilemap_opacity,
            tint: tilemap_tint
          }
           
          // Batching
          var tex = img
          var mat = default_mat
          if (current && current.type == 'sprite_batch' && current.texture == tex && _mat_eq(current.material, mat)) {
            current.sprites.push(sprite)
          } else {
            if (current) batches.push(current)
            current = {type: 'sprite_batch', texture: tex, material: mat, sprites: [sprite]}
          }
        }
      }
    } else {
      if (current) {
        batches.push(current)
        current = null
      }
      batches.push({type: d.type, drawable: d})
    }
  }
  
  if (current) batches.push(current)
  return batches
}

function _mat_eq(a, b) {
  if (!a || !b) return a == b
  return a.blend == b.blend && a.sampler == b.sampler
}

return film2d