var effects_mod = use('effects')
var backend = use('sdl_gpu')
var film2d = use('film2d')

var compositor = {}

// Compile compositor config into render plan
compositor.compile = function(config) {
  var ctx = {
    passes: [],
    targets: {},
    counter: 0,
    screen_size: backend.get_window_size ? backend.get_window_size() : {width: 1280, height: 720},
    
    alloc: function(w, h, hint) {
      var key = (hint || 't') + '_' + text(this.counter++)
      this.targets[key] = {width: w, height: h, key: key}
      return {type: 'target', key: key, width: w, height: h}
    }
  }
  
  var group_effects = config.group_effects || {}
  
  // Clear screen
  if (config.clear)
    push(ctx.passes, {type: 'clear', target: 'screen', color: config.clear})
  
  // Process each plane (supports both 'planes' and legacy 'layers' key)
  var planes = config.planes || config.layers || []
  var i = 0
  var plane = null
  var type = null
  for (i = 0; i < length(planes); i++) {
    plane = planes[i]
    type = plane.type || 'film2d'
    if (type == 'imgui') {
      compile_imgui_layer(plane, ctx)
    } else {
      compile_plane(plane, ctx, group_effects)
    }
  }
  
  return {passes: ctx.passes, targets: ctx.targets, screen_size: ctx.screen_size}
}

function compile_imgui_layer(layer, ctx) {
  push(ctx.passes, {
    type: 'imgui',
    target: 'screen',
    draw: layer.draw
  })
}

function compile_plane(plane_config, ctx, group_effects) {
  var plane_name = plane_config.plane || plane_config.name
  var res = plane_config.resolution || ctx.screen_size
  var camera = plane_config.camera
  var layer_sort = plane_config.layer_sort || {}  // layer -> 'y' or 'explicit'
  
  // Build set of groups used as masks (these should not be drawn directly)
  var mask_groups = {}
  arrfor(array(group_effects), gname => {
    var effects = group_effects[gname].effects || []
    var e = 0
    for (e = 0; e < length(effects); e++) {
      if (effects[e].type == 'mask' && effects[e].mask_group)
        mask_groups[effects[e].mask_group] = true
    }
  })
  
  // Get all sprites in this plane
  var all_sprites = film2d.query({plane: plane_name})
  
  // Add manual drawables
  var di = 0
  if (plane_config.drawables) {
    for (di = 0; di < length(plane_config.drawables); di++)
      push(all_sprites, plane_config.drawables[di])
  }
  
  // Find which sprites belong to groups with effects
  var effect_groups = {}  // group_name -> {sprites: [], effects: []}
  var base_sprites = []

  var si = 0
  var s = null
  var sprite_groups = null
  var assigned = false
  var is_mask_only = false
  var g = 0
  var gname = null
  for (si = 0; si < length(all_sprites); si++) {
    s = all_sprites[si]
    sprite_groups = s.groups || []
    assigned = false
    is_mask_only = length(sprite_groups) > 0

    // First pass: check if sprite has any non-mask group
    for (g = 0; g < length(sprite_groups); g++) {
      gname = sprite_groups[g]
      if (!mask_groups[gname]) {
        is_mask_only = false
        break
      }
    }

    // Second pass: assign to effect groups
    for (g = 0; g < length(sprite_groups); g++) {
      gname = sprite_groups[g]
      if (group_effects[gname]) {
        if (!effect_groups[gname])
          effect_groups[gname] = {sprites: [], effects: group_effects[gname].effects}
        push(effect_groups[gname].sprites, s)
        assigned = true
        break  // Only assign to first matching effect group
      }
    }

    // Add to base sprites if not assigned to effect group and not mask-only
    if (!assigned && !is_mask_only) push(base_sprites, s)
  }
  
  // Allocate plane target
  var plane_target = ctx.alloc(res.width, res.height, plane_config.name)
  
  // Clear plane
  if (plane_config.clear)
    push(ctx.passes, {type: 'clear', target: plane_target, color: plane_config.clear})
  
  // Render each effect group to temp target, apply effects, composite back
  arrfor(array(effect_groups), gname => {
    var eg = effect_groups[gname]
    if (length(eg.sprites) == 0) return
    
    var group_target = ctx.alloc(res.width, res.height, gname + '_content')
    
    // Render group content
    push(ctx.passes, {
      type: 'render',
      renderer: 'film2d',
      drawables: eg.sprites,
      camera: camera,
      target: group_target,
      target_size: res,
      layer_sort: layer_sort,
      clear: {r: 0, g: 0, b: 0, a: 0}
    })
    
    // Apply effects
    var current = group_target
    var e = 0
    var effect = null
    for (e = 0; e < length(eg.effects); e++) {
      effect = eg.effects[e]
      current = apply_effect(ctx, effect, current, {size: res, camera: camera, hint: gname, current_plane: plane_name, group_effects: group_effects})
    }
    
    // Composite result to plane
    push(ctx.passes, {
      type: 'composite',
      source: current,
      dest: plane_target,
      source_size: res,
      dest_size: res,
      blend: 'over'
    })
  })
  
  // Render base sprites (no effects)
  if (length(base_sprites) > 0) {
    push(ctx.passes, {
      type: 'render',
      renderer: 'film2d',
      drawables: base_sprites,
      camera: camera,
      target: plane_target,
      target_size: res,
      layer_sort: layer_sort,
      clear: null  // Don't clear, blend on top
    })
  }
  
  // Composite plane to screen
  push(ctx.passes, {
    type: 'blit_to_screen',
    source: plane_target,
    source_size: res,
    dest_size: ctx.screen_size,
    presentation: plane_config.presentation || 'stretch'
  })
}

function apply_effect(ctx, effect, input, params) {
  var size = params.size
  var camera = params.camera
  var hint = params.hint
  var current_plane = params.current_plane
  var group_effects = params.group_effects
  var output = ctx.alloc(size.width, size.height, hint + '_' + effect.type)

  var bright = null
  var blur1 = null
  var blur2 = null
  var blur_passes = null
  var blur_in = null
  var p = 0
  var mask_group = null
  var mask_sprites = null
  var mask_target = null
  if (effect.type == 'bloom') {
    bright = ctx.alloc(size.width, size.height, hint + '_bright')
    blur1 = ctx.alloc(size.width, size.height, hint + '_blur1')
    blur2 = ctx.alloc(size.width, size.height, hint + '_blur2')

    // Threshold
    push(ctx.passes, {
      type: 'shader_pass',
      shader: 'threshold',
      input: input,
      output: bright,
      uniforms: {threshold: effect.threshold || 0.8, intensity: effect.intensity || 1}
    })

    // Blur passes
    blur_passes = effect.blur_passes || 2
    blur_in = bright
    for (p = 0; p < blur_passes; p++) {
      push(ctx.passes, {type: 'shader_pass', shader: 'blur', input: blur_in, output: blur1, uniforms: {direction: {x: 1, y: 0}, texel_size: {x: 1/size.width, y: 1/size.height}}})
      push(ctx.passes, {type: 'shader_pass', shader: 'blur', input: blur1, output: blur2, uniforms: {direction: {x: 0, y: 1}, texel_size: {x: 1/size.width, y: 1/size.height}}})
      blur_in = blur2
    }
    
    // Composite bloom
    push(ctx.passes, {type: 'composite_textures', base: input, overlay: blur2, output: output, mode: 'add'})
    
  } else if (effect.type == 'mask') {
    mask_group = effect.mask_group
    // Query masks within the same plane to avoid cross-plane mask issues
    mask_sprites = film2d.query({group: mask_group, plane: current_plane})

    if (length(mask_sprites) > 0) {
      mask_target = ctx.alloc(size.width, size.height, hint + '_mask')
      
      // Render mask
      push(ctx.passes, {
        type: 'render',
        renderer: 'film2d',
        drawables: mask_sprites,
        camera: camera,
        target: mask_target,
        target_size: size,
        clear: {r: 0, g: 0, b: 0, a: 0}
      })
      
      // Apply mask
      push(ctx.passes, {
        type: 'apply_mask',
        content: input,
        mask: mask_target,
        output: output,
        mode: effect.channel || 'alpha',
        invert: effect.invert || false
      })
    } else {
      // No mask sprites, pass through
      push(ctx.passes, {type: 'blit', source: input, dest: output})
    }
  } else {
    // Unknown effect, pass through
    push(ctx.passes, {type: 'blit', source: input, dest: output})
  }
  
  return output
}

// Execute compiled plan
compositor.execute = function(plan) {
  var cache = {}
  
  arrfor(array(plan.targets), key => {
    var spec = plan.targets[key]
    cache[key] = backend.get_or_create_target(spec.width, spec.height, key)
  })
  
  function resolve(t) {
    if (t == 'screen') return 'screen'
    if (t && t.type == 'target') return cache[t.key]
    return t
  }
  
  var commands = []

  var i = 0
  var pass = null
  var target = null
  var result = null
  var c = 0
  var rect = null
  var src = null
  var dst = null
  for (i = 0; i < length(plan.passes); i++) {
    pass = plan.passes[i]

    if (pass.type == 'clear') {
      target = resolve(pass.target)
      push(commands, {cmd: 'begin_render', target: target, clear: pass.color})
      push(commands, {cmd: 'end_render'})

    } else if (pass.type == 'render') {
      result = film2d.render({
        drawables: pass.drawables,
        camera: pass.camera,
        target: resolve(pass.target),
        target_size: pass.target_size,
        layer_sort: pass.layer_sort || {},
        clear: pass.clear
      }, backend)
      for (c = 0; c < length(result.commands); c++)
        push(commands, result.commands[c])
        
    } else if (pass.type == 'shader_pass') {
      push(commands, {
        cmd: 'shader_pass',
        shader: pass.shader,
        input: resolve(pass.input),
        output: resolve(pass.output),
        uniforms: pass.uniforms
      })
      
    } else if (pass.type == 'composite_textures') {
      push(commands, {
        cmd: 'composite_textures',
        base: resolve(pass.base),
        overlay: resolve(pass.overlay),
        output: resolve(pass.output),
        mode: pass.mode
      })
      
    } else if (pass.type == 'apply_mask') {
      push(commands, {
        cmd: 'apply_mask',
        content_texture: resolve(pass.content),
        mask_texture: resolve(pass.mask),
        output: resolve(pass.output),
        mode: pass.mode,
        invert: pass.invert
      })
      
    } else if (pass.type == 'composite') {
      push(commands, {
        cmd: 'blit',
        texture: resolve(pass.source),
        target: resolve(pass.dest),
        dst_rect: {x: 0, y: 0, width: pass.dest_size.width, height: pass.dest_size.height}
      })
      
    } else if (pass.type == 'blit_to_screen') {
      rect = _calc_presentation(pass.source_size, pass.dest_size, pass.presentation)
      push(commands, {
        cmd: 'blit',
        texture: resolve(pass.source),
        target: 'screen',
        dst_rect: rect,
        filter: pass.presentation == 'integer_scale' ? 'nearest' : 'linear'
      })
    } else if (pass.type == 'blit') {
      src = resolve(pass.source)
      dst = resolve(pass.dest)
      push(commands, {
        cmd: 'blit',
        texture: src,
        target: dst,
        dst_rect: {x: 0, y: 0, width: dst.width, height: dst.height}
      })
    } else if (pass.type == 'imgui') {
      push(commands, {
        cmd: 'imgui',
        target: resolve(pass.target),
        draw: pass.draw
      })
    }
  }
  
  return {commands: commands, plan: plan}
}

function _calc_presentation(src, dst, mode) {
  if (mode == 'stretch')
    return {x: 0, y: 0, width: dst.width, height: dst.height}

  var sx = 0
  var sy = 0
  var s = 0
  var w = 0
  var h = 0
  var scale = 0
  if (mode == 'integer_scale') {
    sx = floor(dst.width / src.width)
    sy = floor(dst.height / src.height)
    s = max(1, min(sx, sy))
    w = src.width * s
    h = src.height * s
    return {x: (dst.width - w) / 2, y: (dst.height - h) / 2, width: w, height: h}
  }

  // letterbox
  scale = min(dst.width / src.width, dst.height / src.height)
  w = src.width * scale
  h = src.height * scale
  return {x: (dst.width - w) / 2, y: (dst.height - h) / 2, width: w, height: h}
}

return compositor