///|
/// What one command stream would cost this route: how much it draws, and how
/// much it refuses.
///
/// `RendererEvent` carries only present and image-load completions, and both
/// host backends ignore anything else, so adding an "unsupported command" event
/// variant would be a channel with no reader. This is the version that is
/// actually readable: a host, a smoke gate or a test can ask what a frame would
/// draw on any host, because the answer is computed on the CPU without a device.
///
/// `retained_layers_rebuilt`, `retained_layers_updated`, `retained_layers_cached`
/// and `retained_layers_evicted` describe what the cross-frame boundary cache did
/// for this stream, and `shader_effects_fallback_painted` counts a custom effect
/// drawn with the brush its spec declares for renderers that cannot run the
/// shader. None of them is a silent wrong pixel, so all report separately from
/// `refused_count`.
///
/// A one-shot report always rebuilds: `metal_frame_report` has no previous frame
/// to consult, which is the honest answer rather than a limitation — a cache hit
/// is a claim about the frame before this one, and only a renderer that keeps its
/// cache between frames can make it.
pub struct MetalFrameReport {
  items : Int32
  vertices : Int32
  approximated_clips : Int32
  clipped_away : Int32
  text_dropped_glyphs : Int32
  text_runs_dropped : Int32
  image_textures_released : Int32
  image_ops_unresolved : Int32
  layers : Int32
  blend_modes_unsupported : Int32
  blend_modes_approximated : Int32
  filter_approximations : Int32
  retained_layers_rebuilt : Int32
  retained_layers_cached : Int32
  retained_layers_updated : Int32
  retained_layers_evicted : Int32
  shader_effects_fallback_painted : Int32
}

///|
pub fn metal_frame_report(
  commands~ : Array[@core.DrawCommand],
  width~ : Double,
  height~ : Double,
  scale_factor~ : Double = 1.0,
  text_engine~ : NativeTextEngine = native_noop_text_engine(),
) -> MetalFrameReport {
  let size = @core.Size::new(width~, height~)
  let geometry = build_metal_geometry(
    @render_common.lower_draw_commands(commands),
    @core.SurfaceMetrics::new(
      logical_size=size,
      physical_size=@core.Size::new(
        width=width * scale_factor,
        height=height * scale_factor,
      ),
      scale_factor~,
    ),
    MetalFrameContext::{
      cache: @native_text.NativeTextCache::new_with_engine(text_engine),
      atlas: new_metal_glyph_atlas(),
      images: new_metal_image_cache(),
      image_source: @render.HostImageSource::unavailable(),
      context: 0UL,
      async_images: false,
      image_budget_bytes: METAL_IMAGE_MAX_RESIDENT_BYTES,
      retained: new_metal_retained_cache(),
    },
  )
  MetalFrameReport::{
    items: geometry.item_count,
    vertices: geometry.vertex_count,
    approximated_clips: geometry.approximated_clips,
    clipped_away: geometry.clipped_away_ops,
    text_dropped_glyphs: geometry.text_dropped_glyphs,
    text_runs_dropped: geometry.text_runs_dropped,
    image_textures_released: geometry.image_textures_released,
    image_ops_unresolved: geometry.image_ops_unresolved,
    layers: geometry.layers_composited,
    blend_modes_unsupported: geometry.blend_modes_unsupported,
    blend_modes_approximated: geometry.blend_modes_approximated,
    filter_approximations: geometry.filter_approximations,
    retained_layers_rebuilt: geometry.retained_layers_rebuilt,
    retained_layers_cached: geometry.retained_layers_cached,
    retained_layers_updated: geometry.retained_layers_updated,
    retained_layers_evicted: geometry.retained_layers_evicted,
    shader_effects_fallback_painted: geometry.shader_effects_fallback_painted,
  }
}

///|
pub fn MetalFrameReport::draws_anything(self : MetalFrameReport) -> Bool {
  self.items > 0
}

///|
/// Every way this frame departs from what it was asked to paint: approximated
/// clips, glyphs the text engine could not place, blend modes Metal has no state
/// for, and the composites whose fixed-function factors are only exact over an
/// opaque destination. An op this route cannot draw at all is a compile error in
/// `build_metal_range`, not a number here.
pub fn MetalFrameReport::refused_count(self : MetalFrameReport) -> Int32 {
  self.approximated_clips +
  self.text_dropped_glyphs +
  self.text_runs_dropped +
  self.blend_modes_unsupported +
  self.blend_modes_approximated
}