///|
let sun_cached_layer_max_entries : Int = 32

///|
let sun_cached_layer_admission_max_records : Int = 512

///|
pub struct SunCachedLayerAdmissionDiagnostic {
  priv identity_key : String
  priv update_streak : Int
  priv hit_count : Int
  priv skip_reason : String
  priv churn_without_hit : Bool
} derive(Eq, Debug, ToJson)

///|
pub extend SunCachedLayerAdmissionDiagnostic with Debug::{to_repr}

///|
pub extend SunCachedLayerAdmissionDiagnostic with Eq::{not_equal, equal}

///|
pub extend SunCachedLayerAdmissionDiagnostic with ToJson::{to_json}

///|
pub struct SunCachedLayerDiagnostic {
  priv cached_layer_count : Int
  priv max_cached_layer_count : Int
  priv total_hit_count : Int
  priv total_miss_count : Int
  priv total_update_count : Int
  priv total_evict_count : Int
  priv admission_record_count : Int
  priv churn_without_hit_count : Int
  priv cached_layer_keys : Array[String]
  priv admission_records : Array[SunCachedLayerAdmissionDiagnostic]
  priv cache_ready : Bool
} derive(Eq, Debug, ToJson)

///|
pub extend SunCachedLayerDiagnostic with Debug::{to_repr}

///|
pub extend SunCachedLayerDiagnostic with Eq::{not_equal, equal}

///|
pub extend SunCachedLayerDiagnostic with ToJson::{to_json}

///|
pub fn SunCachedLayerAdmissionDiagnostic::identity_key(
  self : SunCachedLayerAdmissionDiagnostic,
) -> String {
  self.identity_key
}

///|
pub fn SunCachedLayerAdmissionDiagnostic::update_streak(
  self : SunCachedLayerAdmissionDiagnostic,
) -> Int {
  self.update_streak
}

///|
pub fn SunCachedLayerAdmissionDiagnostic::hit_count(
  self : SunCachedLayerAdmissionDiagnostic,
) -> Int {
  self.hit_count
}

///|
pub fn SunCachedLayerAdmissionDiagnostic::skip_reason(
  self : SunCachedLayerAdmissionDiagnostic,
) -> String {
  self.skip_reason
}

///|
pub fn SunCachedLayerAdmissionDiagnostic::churn_without_hit(
  self : SunCachedLayerAdmissionDiagnostic,
) -> Bool {
  self.churn_without_hit
}

///|
pub fn SunCachedLayerDiagnostic::cached_layer_count(
  self : SunCachedLayerDiagnostic,
) -> Int {
  self.cached_layer_count
}

///|
pub fn SunCachedLayerDiagnostic::max_cached_layer_count(
  self : SunCachedLayerDiagnostic,
) -> Int {
  self.max_cached_layer_count
}

///|
pub fn SunCachedLayerDiagnostic::total_hit_count(
  self : SunCachedLayerDiagnostic,
) -> Int {
  self.total_hit_count
}

///|
pub fn SunCachedLayerDiagnostic::total_miss_count(
  self : SunCachedLayerDiagnostic,
) -> Int {
  self.total_miss_count
}

///|
pub fn SunCachedLayerDiagnostic::total_update_count(
  self : SunCachedLayerDiagnostic,
) -> Int {
  self.total_update_count
}

///|
pub fn SunCachedLayerDiagnostic::total_evict_count(
  self : SunCachedLayerDiagnostic,
) -> Int {
  self.total_evict_count
}

///|
pub fn SunCachedLayerDiagnostic::admission_record_count(
  self : SunCachedLayerDiagnostic,
) -> Int {
  self.admission_record_count
}

///|
pub fn SunCachedLayerDiagnostic::churn_without_hit_count(
  self : SunCachedLayerDiagnostic,
) -> Int {
  self.churn_without_hit_count
}

///|
pub fn SunCachedLayerDiagnostic::cached_layer_keys(
  self : SunCachedLayerDiagnostic,
) -> Array[String] {
  self.cached_layer_keys
}

///|
pub fn SunCachedLayerDiagnostic::admission_records(
  self : SunCachedLayerDiagnostic,
) -> Array[SunCachedLayerAdmissionDiagnostic] {
  self.admission_records
}

///|
pub fn SunCachedLayerDiagnostic::cache_ready(
  self : SunCachedLayerDiagnostic,
) -> Bool {
  self.cache_ready
}

///|
pub struct SunCachedLayerAdmissionRecord {
  identity_key : String
  mut update_streak : Int
  mut hit_count : Int
  mut skip_reason : String
  mut last_revision : Int
}

///|
priv struct SunCachedLayerFrameStats {
  mut cache_hit_count : Int
  mut cache_miss_count : Int
  mut cache_update_count : Int
  mut cache_evict_count : Int
}

///|
fn SunCachedLayerFrameStats::new() -> SunCachedLayerFrameStats {
  {
    cache_hit_count: 0,
    cache_miss_count: 0,
    cache_update_count: 0,
    cache_evict_count: 0,
  }
}

///|
fn SunCachedLayerAdmissionRecord::new(
  identity_key : String,
) -> SunCachedLayerAdmissionRecord {
  {
    identity_key,
    update_streak: 0,
    hit_count: 0,
    skip_reason: "",
    last_revision: -1,
  }
}

///|
fn SunCachedLayerAdmissionRecord::record_update(
  self : SunCachedLayerAdmissionRecord,
  revision : Int,
) -> Unit {
  if self.last_revision != revision {
    self.update_streak = self.update_streak + 1
    self.last_revision = revision
  }
}

///|
fn SunCachedLayerAdmissionRecord::record_hit(
  self : SunCachedLayerAdmissionRecord,
) -> Unit {
  self.hit_count = self.hit_count + 1
}

///|
fn SunCachedLayerAdmissionRecord::churn_without_hit(
  self : SunCachedLayerAdmissionRecord,
) -> Bool {
  self.update_streak > 2 && self.hit_count == 0
}

///|
fn SunCachedLayerAdmissionRecord::to_diagnostic(
  self : SunCachedLayerAdmissionRecord,
) -> SunCachedLayerAdmissionDiagnostic {
  {
    identity_key: self.identity_key,
    update_streak: self.update_streak,
    hit_count: self.hit_count,
    skip_reason: self.skip_reason,
    churn_without_hit: self.churn_without_hit(),
  }
}

///|
fn SunCachedLayerFrameStats::to_render_frame_result(
  self : SunCachedLayerFrameStats,
  presented~ : Bool,
) -> @render.RenderFrameResult {
  @render.RenderFrameResult::new(
    presented~,
    cache_hit_count=self.cache_hit_count,
    cache_miss_count=self.cache_miss_count,
    cache_update_count=self.cache_update_count,
    cache_evict_count=self.cache_evict_count,
  )
}

///|
pub fn SunCachedLayerDiagnostic::summary(
  self : SunCachedLayerDiagnostic,
) -> String {
  let label = if self.cache_ready {
    "Sun cached-layer audit ready"
  } else {
    "Sun cached-layer audit pending"
  }
  let base = "\{label}: entries=\{self.cached_layer_count}/\{self.max_cached_layer_count} hits=\{self.total_hit_count} misses=\{self.total_miss_count}"
  base +
  " updates=\{self.total_update_count} evicts=\{self.total_evict_count} churn_without_hit=\{self.churn_without_hit_count}"
}

///|
pub fn SunRasterRenderer::cached_layer_diagnostic(
  self : SunRasterRenderer,
) -> SunCachedLayerDiagnostic {
  let admission_records : Array[SunCachedLayerAdmissionDiagnostic] = []
  let mut churn_without_hit_count = 0
  for record in self.cached_layer_admissions {
    let diagnostic = record.to_diagnostic()
    if diagnostic.churn_without_hit {
      churn_without_hit_count = churn_without_hit_count + 1
    }
    admission_records.push(diagnostic)
  }
  {
    cached_layer_count: self.cached_layer_order.length(),
    max_cached_layer_count: sun_cached_layer_max_entries,
    total_hit_count: self.cached_layer_hit_count,
    total_miss_count: self.cached_layer_miss_count,
    total_update_count: self.cached_layer_update_count,
    total_evict_count: self.cached_layer_evict_count,
    admission_record_count: self.cached_layer_admissions.length(),
    churn_without_hit_count,
    cached_layer_keys: copy_sun_cached_layer_keys(self.cached_layer_order),
    admission_records,
    cache_ready: self.cached_layer_order.length() <=
    sun_cached_layer_max_entries,
  }
}

///|
fn SunRasterRenderer::cached_layer_admission_record(
  self : SunRasterRenderer,
  identity_key : String,
) -> SunCachedLayerAdmissionRecord {
  for record in self.cached_layer_admissions {
    if record.identity_key == identity_key {
      return record
    }
  }
  let record = SunCachedLayerAdmissionRecord::new(identity_key)
  self.cached_layer_admissions.push(record)
  self.prune_cached_layer_admission_records(identity_key)
  record
}

///|
fn SunRasterRenderer::prune_cached_layer_admission_records(
  self : SunRasterRenderer,
  protected_identity_key : String,
) -> Unit {
  while self.cached_layer_admissions.length() >
        sun_cached_layer_admission_max_records {
    let mut removed = false
    let mut index = 0
    while index < self.cached_layer_admissions.length() && !removed {
      if self.cached_layer_admissions[index].identity_key !=
        protected_identity_key {
        ignore(self.cached_layer_admissions.remove(index))
        removed = true
      } else {
        index = index + 1
      }
    }
    if !removed {
      return
    }
  }
}

///|
fn SunRasterRenderer::cached_layer_admission_skip_reason(
  self : SunRasterRenderer,
  admission : SunCachedLayerAdmissionRecord,
) -> String? {
  let _ = self
  if admission.update_streak > 2 && admission.hit_count == 0 {
    Some("churn-without-hit")
  } else {
    None
  }
}

///|
fn sun_find_cached_layer_end(
  commands : Array[@render_common.PlanOp],
  start : Int,
  key : String,
) -> Int? {
  let mut depth = 0
  for index in start.. depth = depth + 1
      EndRetainedLayer(end_key) =>
        if depth == 0 {
          if end_key == key {
            return Some(index)
          } else {
            return None
          }
        } else {
          depth = depth - 1
        }
      _ => ()
    }
  }
  None
}

///|
fn SunRasterRenderer::render_cached_command_range(
  self : SunRasterRenderer,
  commands : Array[@render_common.PlanOp],
  start : Int,
  end : Int,
  canvas_scopes : Array[@render_common.PlanScopeKind],
  opacity_stack : Array[Double],
  blend_mode_stack : Array[@sun_graphics.BlendMode],
  layer_spec_stack : Array[SunLayerScope],
  rounded_clip_transform_stack : Array[@sun_graphics.Transform],
  stats : SunCachedLayerFrameStats,
) -> Bool {
  let mut index = start
  while index < end {
    match commands[index] {
      BeginRetainedLayer(spec) =>
        match sun_find_cached_layer_end(commands, index + 1, spec.key) {
          Some(layer_end) => {
            if !self.render_or_draw_cached_layer(
                spec,
                commands,
                index + 1,
                layer_end,
                sun_current_opacity(opacity_stack),
                stats,
              ) {
              return false
            }
            index = layer_end + 1
          }
          None => {
            self.record_unsupported_command(
              "BeginRetainedLayer",
              "cached layer command stream ended without matching EndRetainedLayer for \{spec.key}",
            )
            return false
          }
        }
      EndRetainedLayer(_) => {
        self.record_unsupported_command(
          "EndRetainedLayer", "EndRetainedLayer encountered without matching BeginRetainedLayer",
        )
        return false
      }
      command => {
        self.render_command(
          command, canvas_scopes, opacity_stack, blend_mode_stack, layer_spec_stack,
          rounded_clip_transform_stack,
        )
        index = index + 1
      }
    }
  }
  true
}

///|
fn SunRasterRenderer::render_or_draw_cached_layer(
  self : SunRasterRenderer,
  spec : @core.RetainedLayerSpec,
  commands : Array[@render_common.PlanOp],
  child_start : Int,
  child_end : Int,
  opacity : Double,
  stats : SunCachedLayerFrameStats,
) -> Bool {
  if sun_cached_layer_is_empty(spec) {
    return true
  }
  let cache_key = sun_cached_layer_cache_key(spec, self.metrics.scale_factor)
  if self.cached_layer_pixmaps.contains(cache_key) {
    let cached_rev = self.cached_layer_revisions.get(cache_key)
    let cached_scale = self.cached_layer_scales.get(cache_key)
    if cached_rev is Some(revision) &&
      revision == spec.content_revision &&
      cached_scale is Some(scale) &&
      scale == self.metrics.scale_factor {
      let pixmap = self.cached_layer_pixmaps[cache_key]
      self.draw_cached_layer_pixmap(pixmap, spec.frame, opacity)
      self.cached_layer_admission_record(cache_key).record_hit()
      self.cached_layer_hit_count = self.cached_layer_hit_count + 1
      stats.cache_hit_count = stats.cache_hit_count + 1
      return true
    }
  }
  stats.cache_miss_count = stats.cache_miss_count + 1
  self.update_cached_layer(
    spec, commands, child_start, child_end, opacity, stats,
  )
}

///|
fn SunRasterRenderer::update_cached_layer(
  self : SunRasterRenderer,
  spec : @core.RetainedLayerSpec,
  commands : Array[@render_common.PlanOp],
  child_start : Int,
  child_end : Int,
  opacity : Double,
  stats : SunCachedLayerFrameStats,
) -> Bool {
  guard !sun_cached_layer_is_empty(spec) else { return true }
  let physical_w = sun_cached_layer_dim(
    spec.frame.size.width,
    self.metrics.scale_factor,
  )
  let physical_h = sun_cached_layer_dim(
    spec.frame.size.height,
    self.metrics.scale_factor,
  )
  guard physical_w > 0 && physical_h > 0 else { return true }
  let layer_canvas = @sun_graphics.Canvas::new(physical_w, physical_h)
  layer_canvas.clear(@sun_graphics.Color::new(r=0, g=0, b=0, a=0))
  layer_canvas.scale(sx=self.metrics.scale_factor, sy=self.metrics.scale_factor)
  layer_canvas.translate(
    tx=0.0 - spec.frame.origin.x,
    ty=0.0 - spec.frame.origin.y,
  )
  let saved_canvas = self.canvas
  let saved_canvas_stack = self.canvas_stack
  let saved_filter_stack = self.filter_stack
  let saved_blur_radius_stack = self.blur_radius_stack
  let saved_rounded_clip_stack = self.rounded_clip_stack
  self.canvas = layer_canvas
  self.canvas_stack = []
  self.filter_stack = []
  self.blur_radius_stack = []
  self.rounded_clip_stack = []
  let child_scopes : Array[@render_common.PlanScopeKind] = []
  let child_opacity : Array[Double] = []
  let child_blend : Array[@sun_graphics.BlendMode] = []
  let child_layer_specs : Array[SunLayerScope] = []
  let child_rounded_clip_transforms : Array[@sun_graphics.Transform] = []
  let complete = self.render_cached_command_range(
    commands, child_start, child_end, child_scopes, child_opacity, child_blend, child_layer_specs,
    child_rounded_clip_transforms, stats,
  )
  if !child_scopes.is_empty() {
    for scope in child_scopes {
      self.record_unsupported_command(
        @render_common.plan_scope_push_command(scope),
        "cached layer command stream ended with an open \{@render_common.plan_scope_name(scope)} scope",
      )
    }
    while !self.canvas_stack.is_empty() {
      match self.canvas_stack.pop() {
        Some(parent) => self.canvas = parent
        None => ()
      }
    }
    self.filter_stack.clear()
    self.blur_radius_stack.clear()
    self.rounded_clip_stack.clear()
    child_rounded_clip_transforms.clear()
    while !child_blend.is_empty() {
      ignore(child_blend.pop())
    }
    while !child_layer_specs.is_empty() {
      ignore(child_layer_specs.pop())
    }
  }
  if !complete {
    self.canvas = saved_canvas
    self.canvas_stack = saved_canvas_stack
    self.filter_stack = saved_filter_stack
    self.blur_radius_stack = saved_blur_radius_stack
    self.rounded_clip_stack = saved_rounded_clip_stack
    return false
  }
  let pixmap = self.canvas.get_pixmap()
  let key = sun_cached_layer_cache_key(spec, self.metrics.scale_factor)
  let admission = self.cached_layer_admission_record(key)
  admission.record_update(spec.content_revision)
  match self.cached_layer_admission_skip_reason(admission) {
    Some(reason) => {
      admission.skip_reason = reason
      self.remove_cached_layer_entry(key)
      self.canvas = saved_canvas
      self.canvas_stack = saved_canvas_stack
      self.filter_stack = saved_filter_stack
      self.blur_radius_stack = saved_blur_radius_stack
      self.rounded_clip_stack = saved_rounded_clip_stack
      self.draw_cached_layer_pixmap(pixmap, spec.frame, opacity)
      return true
    }
    None => admission.skip_reason = ""
  }
  self.cached_layer_pixmaps[key] = pixmap
  self.cached_layer_revisions[key] = spec.content_revision
  self.cached_layer_scales[key] = self.metrics.scale_factor
  self.remove_cached_layer_order_entries(key)
  self.cached_layer_order.push(key)
  let evicted = self.evict_cached_layers_if_needed()
  stats.cache_evict_count = stats.cache_evict_count + evicted
  self.cached_layer_evict_count = self.cached_layer_evict_count + evicted
  stats.cache_update_count = stats.cache_update_count + 1
  self.cached_layer_update_count = self.cached_layer_update_count + 1
  self.canvas = saved_canvas
  self.canvas_stack = saved_canvas_stack
  self.filter_stack = saved_filter_stack
  self.blur_radius_stack = saved_blur_radius_stack
  self.rounded_clip_stack = saved_rounded_clip_stack
  self.draw_cached_layer_pixmap(pixmap, spec.frame, opacity)
  self.cached_layer_miss_count = self.cached_layer_miss_count + 1
  true
}

///|
fn SunRasterRenderer::draw_cached_layer_pixmap(
  self : SunRasterRenderer,
  pixmap : @sun_graphics.Pixmap,
  frame : @core.Rect,
  opacity : Double,
) -> Unit {
  let src_rect = @sun_graphics.PixelRect::new(
    x=0,
    y=0,
    width=pixmap.get_width(),
    height=pixmap.get_height(),
  )
  self.canvas.draw_transformed_quality_pixmap_rect(
    pixmap,
    src_rect,
    frame.origin.x,
    frame.origin.y,
    frame.size.width,
    frame.size.height,
    @sun_graphics.Paint::new().set_opacity(opacity),
    Balanced,
  )
}

///|
fn SunRasterRenderer::evict_cached_layers_if_needed(
  self : SunRasterRenderer,
) -> Int {
  let mut evicted = 0
  while self.cached_layer_order.length() > sun_cached_layer_max_entries {
    if self.cached_layer_order.length() > 0 {
      let old_key = self.cached_layer_order[0]
      ignore(self.cached_layer_order.remove(0))
      ignore(self.cached_layer_pixmaps.remove(old_key))
      ignore(self.cached_layer_revisions.remove(old_key))
      ignore(self.cached_layer_scales.remove(old_key))
      evicted = evicted + 1
    } else {
      break
    }
  }
  evicted
}

///|
fn SunRasterRenderer::remove_cached_layer_entry(
  self : SunRasterRenderer,
  key : String,
) -> Unit {
  self.remove_cached_layer_order_entries(key)
  ignore(self.cached_layer_pixmaps.remove(key))
  ignore(self.cached_layer_revisions.remove(key))
  ignore(self.cached_layer_scales.remove(key))
}

///|
fn SunRasterRenderer::remove_cached_layer_order_entries(
  self : SunRasterRenderer,
  key : String,
) -> Unit {
  let mut index = 0
  while index < self.cached_layer_order.length() {
    if self.cached_layer_order[index] == key {
      ignore(self.cached_layer_order.remove(index))
    } else {
      index = index + 1
    }
  }
}

///|
fn SunRasterRenderer::clear_cached_layers(self : SunRasterRenderer) -> Unit {
  self.cached_layer_pixmaps = Map([])
  self.cached_layer_revisions = Map([])
  self.cached_layer_scales = Map([])
  self.cached_layer_order = []
  self.cached_layer_hit_count = 0
  self.cached_layer_miss_count = 0
  self.cached_layer_update_count = 0
  self.cached_layer_evict_count = 0
  self.cached_layer_admissions.clear()
}

///|
fn copy_sun_cached_layer_keys(keys : Array[String]) -> Array[String] {
  let copied = []
  for key in keys {
    copied.push(key)
  }
  copied
}

///|
fn sun_cached_layer_cache_key(
  spec : @core.RetainedLayerSpec,
  scale : Double,
) -> String {
  let physical_w = sun_cached_layer_dim(spec.frame.size.width, scale)
  let physical_h = sun_cached_layer_dim(spec.frame.size.height, scale)
  "\{spec.key}|scale=\{scale}|pw=\{physical_w}|ph=\{physical_h}"
}

///|
fn sun_cached_layer_is_empty(spec : @core.RetainedLayerSpec) -> Bool {
  spec.frame.size.width <= 0.0 || spec.frame.size.height <= 0.0
}

///|
fn sun_cached_layer_dim(value : Double, scale : Double) -> Int {
  (value * scale).ceil().max(1.0).to_int()
}