// fx_graph.cm - Low-level compositing graph primitives
//
// This module provides the low-level pass primitives for compositing.
// It does NOT know about scene trees, sprites, tilemaps, etc. - that's renderer territory.
//
// Core node types:
//
// render_view
//   Render a scene using a renderer (film2d, etc.) into a target.
//   Params:
//     root        - Scene tree root node
//     camera      - Camera object
//     target      - Target spec: {width, height} or 'screen' or existing target
//     clear_color - Optional RGBA clear color
//     renderer    - Renderer module to use (defaults to film2d)
//   Output: {target, commands}
//
// composite
//   Combine two inputs with a blend mode.
//   Params:
//     base     - Base layer (output from another node)
//     overlay  - Overlay layer (output from another node)
//     mode     - 'over' (default), 'add', 'multiply'
//     opacity  - 0-1, overlay opacity
//   Output: {target, commands}
//
// mask
//   Apply a mask to content.
//   Params:
//     content  - Content to mask (output from another node)
//     mask     - Mask source (output from another node)
//     mode     - 'binary' | 'alpha' (default 'alpha')
//     invert   - bool, invert mask
//   Output: {target, commands}
//
// blit
//   Copy/scale an image into a target.
//   Params:
//     input    - Source (output from another node or texture)
//     target   - Destination target spec or 'screen'
//     dst_rect - {x, y, width, height} destination rectangle
//     filter   - 'nearest' | 'linear'
//   Output: {target, commands}
//
// shader_pass
//   Apply a shader effect (blur, threshold, crt, etc.)
//   Params:
//     shader   - Shader name ('blur', 'threshold', 'crt')
//     input    - Input texture/target
//     output   - Output target spec
//     uniforms - Shader uniforms
//   Output: {target, commands}
//
// present
//   Present to display.
//   Params:
//     input    - Final image to present
//   Output: {commands}

var fx_graph = {}

fx_graph.add_node = function(type, params) {
  params = params || {}
  var node = {
    id: this.next_id++,
    type: type,
    params: params,
    output: {node_id: this.next_id - 1, slot: 'output'}
  }
  this.nodes.push(node)
  return node
}

fx_graph.set_output = function(output_handle) {
  this.output_node = output_handle
}

// Execute graph using backend
fx_graph.execute = function(backend) {
  var sorted = this.topological_sort()
  var node_outputs = {}
  var all_commands = []
  
  for (var node of sorted) {
    var executor = NODE_EXECUTORS[node.type]
    if (!executor) {
      log.console(`fx_graph: No executor for node type: ${node.type}`)
      continue
    }
    
    var resolved_params = this.resolve_inputs(node.params, node_outputs)
    resolved_params._node_id = node.id
    
    var result = executor(resolved_params, backend)
    node_outputs[node.id] = result
    
    // Collect commands from this node
    if (result && result.commands) {
      for (var cmd of result.commands) {
        all_commands.push(cmd)
      }
    }
  }
  
  return {commands: all_commands}
}

fx_graph.resolve_inputs = function(params, node_outputs) {
  var resolved = {}
  for (var key in params) {
    var value = params[key]
    if (value && value.node_id != null)
      resolved[key] = node_outputs[value.node_id]
    else
      resolved[key] = value
  }
  return resolved
}

fx_graph.topological_sort = function() {
  // Nodes are added in dependency order by construction - you can't reference
  // a node's output before the node is created. So insertion order is already
  // a valid topological order.
  return this.nodes
}

// ========================================================================
// NODE EXECUTORS
// ========================================================================

var NODE_EXECUTORS = {}

// render_view: Render scene tree to target using a renderer
NODE_EXECUTORS.render_view = function(params, backend) {
  var root = params.root
  var camera = params.camera
  var target_spec = params.target
  var clear_color = params.clear_color
  var renderer = params.renderer
  
  // Determine target
  var target
  if (target_spec == 'screen') {
    target = 'screen'
  } else if (target_spec && target_spec.texture) {
    // Reuse existing target
    target = target_spec
  } else {
    // Allocate render target
    target = backend.get_or_create_target(
      target_spec.width,
      target_spec.height,
      'view_' + params._node_id
    )
  }
  
  // Get target size
  var target_size = target == 'screen' 
    ? backend.get_window_size()
    : {width: target.width, height: target.height}
  
  // Use renderer if provided, otherwise use default film2d
  if (renderer && renderer.render) {
    return renderer.render({
      root: root,
      camera: camera,
      target: target,
      target_size: target_size,
      clear: clear_color
    }, backend)
  }
  
  // Fallback: use film2d module
  var film2d = use('prosperon/film2d')
  return film2d.render({
    root: root,
    camera: camera,
    target: target,
    target_size: target_size,
    clear: clear_color
  }, backend)
}

// composite: Combine two layers
NODE_EXECUTORS.composite = function(params, backend) {
  var base = params.base
  var overlay = params.overlay
  var mode = params.mode || 'over'
  var opacity = params.opacity != null ? params.opacity : 1
  
  // Optimization: if overlay opacity is 0, just return base
  if (opacity == 0) return base
  
  // Optimization: if base is null/empty, just return overlay
  if (!base || !base.target) return overlay
  
  // Optimization: if overlay is null/empty, just return base
  if (!overlay || !overlay.target) return base
  
  var target = backend.get_or_create_target(
    base.target.width,
    base.target.height,
    'composite_' + params._node_id
  )
  
  // Emit composite_textures command (handled outside render pass)
  var commands = []
  commands.push({
    cmd: 'composite_textures',
    base: base.target,
    overlay: overlay.target,
    output: target,
    mode: mode,
    opacity: opacity
  })
  
  return {target: target, commands: commands}
}

// mask: Apply mask to content
NODE_EXECUTORS.mask = function(params, backend) {
  var content = params.content
  var mask = params.mask
  var mode = params.mode || 'alpha'
  var invert = params.invert || false
  
  if (!content || !content.target) return {target: null, commands: []}
  if (!mask || !mask.target) return content
  
  var target = backend.get_or_create_target(
    content.target.width,
    content.target.height,
    'mask_' + params._node_id
  )
  
  // Emit apply_mask command (handled via shader pass outside render pass)
  var commands = []
  commands.push({
    cmd: 'apply_mask',
    content_texture: content.target,
    mask_texture: mask.target,
    output: target,
    mode: mode,
    invert: invert
  })
  
  return {target: target, commands: commands}
}

// clip_rect: Apply scissor clipping
NODE_EXECUTORS.clip_rect = function(params, backend) {
  var input = params.input
  var rect = params.rect
  
  if (!input) return {target: null, commands: []}
  
  // Clip doesn't need a new target, just adds scissor to commands
  var commands = input.commands ? input.commands.slice() : []
  
  // Insert scissor after begin_render
  var insert_idx = 0
  for (var i = 0; i < commands.length; i++) {
    if (commands[i].cmd == 'begin_render') {
      insert_idx = i + 1
      break
    }
  }
  
  commands.splice(insert_idx, 0, {cmd: 'scissor', rect: rect})
  
  // Add scissor reset before end_render
  for (var i = commands.length - 1; i >= 0; i--) {
    if (commands[i].cmd == 'end_render') {
      commands.splice(i, 0, {cmd: 'scissor', rect: null})
      break
    }
  }
  
  return {target: input.target, commands: commands}
}

// blit: Copy/scale image to target
NODE_EXECUTORS.blit = function(params, backend) {
  var input = params.input
  var target_spec = params.target
  var dst_rect = params.dst_rect
  var filter = params.filter || 'nearest'
  
  var src_target = input && input.target ? input.target : input
  if (!src_target) return {target: null, commands: []}
  
  var target
  if (target_spec == 'screen') {
    target = 'screen'
  } else if (target_spec && target_spec.target) {
    // Output reference from another node - use its target
    target = target_spec.target
  } else if (target_spec && target_spec.texture) {
    // Already a render target
    target = target_spec
  } else if (target_spec && target_spec.width) {
    // Target spec - use a consistent key based on the spec itself
    var key = `blit_${target_spec.width}x${target_spec.height}`
    target = backend.get_or_create_target(target_spec.width, target_spec.height, key)
  } else {
    return {target: null, commands: []}
  }
  
  var commands = []
  commands.push({
    cmd: 'blit',
    texture: src_target,
    target: target,
    dst_rect: dst_rect,
    filter: filter
  })
  
  return {target: target, commands: commands}
}

// present: Present to display
NODE_EXECUTORS.present = function(params, backend) {
  var input = params.input
  
  var commands = []
  commands.push({cmd: 'present'})
  
  return {commands: commands}
}

// shader_pass: Generic shader pass
NODE_EXECUTORS.shader_pass = function(params, backend) {
  var input = params.input
  var shader = params.shader
  var uniforms = params.uniforms || {}
  var output_spec = params.output
  
  if (!input || !input.target) return {target: null, commands: []}
  
  var src = input.target
  var target
  
  if (output_spec == 'screen') {
    target = 'screen'
  } else if (output_spec && output_spec.texture) {
    target = output_spec
  } else {
    // Default to input size if not specified
    var w = output_spec && output_spec.width ? output_spec.width : src.width
    var h = output_spec && output_spec.height ? output_spec.height : src.height
    target = backend.get_or_create_target(w, h, 'shader_' + shader + '_' + params._node_id)
  }
  
  var commands = []
  commands.push({
    cmd: 'shader_pass',
    shader: shader,
    input: src,
    output: target,
    uniforms: uniforms
  })
  
  return {target: target, commands: commands}
}

// ========================================================================
// UTILITY: Fit rectangle to screen with aspect preservation
// ========================================================================

function fit_to_screen(target_spec, window_size) {
  var src_aspect = target_spec.width / target_spec.height
  var dst_aspect = window_size.width / window_size.height
  
  var scale = src_aspect > dst_aspect
    ? window_size.width / target_spec.width
    : window_size.height / target_spec.height
  
  var w = target_spec.width * scale
  var h = target_spec.height * scale
  var x = (window_size.width - w) / 2
  var y = (window_size.height - h) / 2
  
  return {x: x, y: y, width: w, height: h}
}

// Export fit_to_screen for external use
fx_graph.fit_to_screen = fit_to_screen
function make_fxgraph() {
  return meme(fx_graph, {
    nodes: [],
    output_node: null,
    next_id: 0
  })
}

make_fxgraph.fit_to_screen = fit_to_screen

return make_fxgraph
