///|
/// Command queue + batch-merge layer.
///
/// `CommandQueue` is the trait a renderer enqueues `DrawTrianglesCommand`
/// onto; `SimpleCommandQueue` is the reference implementation that merges
/// adjacent compatible commands within a vertex budget.
///
/// `flush_commands` drives a `GraphicsDriver` from a queue, and
/// `clear_screen` performs the minimum begin/end pair without any draws.
///
/// Ebiten refs:
/// - internal/graphicscommand/commandqueue.go (EnqueueDrawTrianglesCommand, Flush)

///|
/// A renderer-facing buffer of pending `DrawTrianglesCommand`s.
///
/// Renderers `enqueue_draw_triangles` during their `draw` pass and the
/// frame driver later calls `flush` to consume them. The flush phase is
/// where an implementation may merge adjacent compatible commands (same
/// pipeline / shader / blend / images / uniforms) to reduce the number
/// of backend draw calls.
///
/// `SimpleCommandQueue` is the reference implementation; bring your own
/// if you need a different merge policy.
pub(open) trait CommandQueue {
  /// Append one command to the back of the queue.
  /// Ebiten ref: commandQueue.EnqueueDrawTrianglesCommand.
  fn enqueue_draw_triangles(Self, command : DrawTrianglesCommand) -> Unit

  /// Drain the queue and return the (possibly merged) commands in
  /// submission order. The queue is empty after this call.
  /// Ebiten ref: commandQueue.Flush / FlushCommands.
  fn flush(Self) -> Array[DrawTrianglesCommand]
}

///|
pub struct SimpleCommandQueue {
  mut commands : Array[DrawTrianglesCommand]
} derive(Debug)

///|
fn blend_factor_eq(lhs : BlendFactor, rhs : BlendFactor) -> Bool {
  BlendFactor::to_int(lhs) == BlendFactor::to_int(rhs)
}

///|
fn blend_operation_eq(lhs : BlendOperation, rhs : BlendOperation) -> Bool {
  BlendOperation::to_int(lhs) == BlendOperation::to_int(rhs)
}

///|
fn blend_equation_eq(lhs : BlendEquation, rhs : BlendEquation) -> Bool {
  blend_factor_eq(lhs.src_factor_rgb, rhs.src_factor_rgb) &&
  blend_factor_eq(lhs.dst_factor_rgb, rhs.dst_factor_rgb) &&
  blend_operation_eq(lhs.op_rgb, rhs.op_rgb) &&
  blend_factor_eq(lhs.src_factor_alpha, rhs.src_factor_alpha) &&
  blend_factor_eq(lhs.dst_factor_alpha, rhs.dst_factor_alpha) &&
  blend_operation_eq(lhs.op_alpha, rhs.op_alpha)
}

///|
fn blend_mode_eq(lhs : BlendMode, rhs : BlendMode) -> Bool {
  match lhs {
    BlendMode::Copy =>
      match rhs {
        BlendMode::Copy => true
        _ => false
      }
    BlendMode::Alpha =>
      match rhs {
        BlendMode::Alpha => true
        _ => false
      }
    BlendMode::Add =>
      match rhs {
        BlendMode::Add => true
        _ => false
      }
    BlendMode::Multiply =>
      match rhs {
        BlendMode::Multiply => true
        _ => false
      }
    BlendMode::Custom(eq_l) =>
      match rhs {
        BlendMode::Custom(eq_r) => blend_equation_eq(eq_l, eq_r)
        _ => false
      }
  }
}

///|
fn int_array_eq(lhs : Array[Int], rhs : Array[Int]) -> Bool {
  if lhs.length() != rhs.length() {
    return false
  }
  for i in 0.. Bool {
  // Instanced commands cannot be merged
  if lhs.instance_count != 1 || rhs.instance_count != 1 {
    return false
  }
  let lhs_explicit = lhs.vertex_data.length() > 0 || lhs.indices.length() > 0
  let rhs_explicit = rhs.vertex_data.length() > 0 || rhs.indices.length() > 0
  lhs_explicit == rhs_explicit &&
  lhs.vertex_data.length() + rhs.vertex_data.length() <= max_merge_vertex_floats &&
  lhs.dst.id == rhs.dst.id &&
  lhs.shader.id == rhs.shader.id &&
  blend_mode_eq(lhs.blend, rhs.blend) &&
  lhs.pipeline_id == rhs.pipeline_id &&
  lhs.uniform_hash == rhs.uniform_hash &&
  lhs.index_offset == rhs.index_offset &&
  int_array_eq(lhs.src_image_ids, rhs.src_image_ids) &&
  int_array_eq(lhs.uniform_dwords, rhs.uniform_dwords)
}

///|
fn can_merge_draw_triangles_with_vertex_budget(
  lhs : DrawTrianglesCommand,
  rhs : DrawTrianglesCommand,
  lhs_vertex_float_count : Int,
) -> Bool {
  can_merge_draw_triangles(lhs, rhs) &&
  lhs_vertex_float_count + rhs.vertex_data.length() <= max_merge_vertex_floats
}

///|
fn merge_draw_triangles_range(
  commands : Array[DrawTrianglesCommand],
  start : Int,
  end : Int,
) -> DrawTrianglesCommand {
  let base = commands[start]
  let explicit = base.vertex_data.length() > 0 || base.indices.length() > 0
  let merged_regions : Array[DstRegion] = []
  for i in start.. SimpleCommandQueue {
  { commands: [] }
}

///|
pub fn[T : GraphicsDriver, Q : CommandQueue] flush_commands(
  driver : T,
  queue : Q,
  present : Bool,
  clear_color? : Color,
) -> Unit raise {
  let commands = queue.flush()
  if commands.length() == 0 && !present {
    ()
  } else {
    let color = match clear_color {
      Some(c) => c
      None => { r: 0.0, g: 0.0, b: 0.0, a: 1.0 }
    }
    driver.begin({ clear_color: color, clear_enabled: true, present })
    for command in commands {
      driver.draw_triangles(command)
    }
    driver.end(present)
  }
}

///|
pub fn[T : GraphicsDriver] clear_screen(
  driver : T,
  color : Color,
) -> Unit raise {
  driver.begin({ clear_color: color, clear_enabled: true, present: false })
  driver.end(false)
}