///|
fn SunRasterRenderer::draw_image(
  self : SunRasterRenderer,
  image : @core.ImageRun,
  opacity : Double,
) -> Unit {
  guard image.frame.size.width > 0.0 && image.frame.size.height > 0.0 else {
    return
  }
  match self.sun_image_for_source(image.source) {
    Some(pixmap) => self.draw_image_pixmap(image, pixmap, opacity)
    None => self.draw_image_placeholder(image, opacity)
  }
}

///|
fn SunRasterRenderer::sun_image_for_source(
  self : SunRasterRenderer,
  source : String,
) -> @sun_graphics.Pixmap? {
  match self.resources.get_image_cache().get(source) {
    Some(pixmap) => Some(pixmap)
    None =>
      if self.async_image_loading {
        self.sun_async_image_for_source(source)
      } else {
        self.sun_sync_image_for_source(source)
      }
  }
}

///|
fn SunRasterRenderer::sun_sync_image_for_source(
  self : SunRasterRenderer,
  source : String,
) -> @sun_graphics.Pixmap? {
  match self.image_lifecycle.lookup(source) {
    Some({ status: ImageFailed, .. }) =>
      if !sun_image_failed_record_can_retry(source) {
        return None
      }
    _ => ()
  }
  match sun_image_source_bytes(source) {
    Some(_) => self.mark_sun_image_loading_if_needed(source)
    None => ()
  }
  match self.decode_image_source_with_cache(source) {
    Ok(pixmap) => {
      self.mark_sun_image_ready_if_needed(source, pixmap)
      Some(pixmap)
    }
    Err(diagnostic) => {
      self.mark_sun_image_failed_if_needed(source, diagnostic)
      self.record_unsupported_command("DrawImage", diagnostic)
      None
    }
  }
}

///|
fn SunRasterRenderer::sun_async_image_for_source(
  self : SunRasterRenderer,
  source : String,
) -> @sun_graphics.Pixmap? {
  match self.image_lifecycle.lookup(source) {
    Some({ status: ImageLoading, .. }) => None
    Some({ status: ImageReady, .. }) =>
      match self.decode_image_source_with_cache(source) {
        Ok(pixmap) => Some(pixmap)
        Err(diagnostic) => {
          self.mark_sun_image_failed_if_needed(source, diagnostic)
          self.record_unsupported_command("DrawImage", diagnostic)
          None
        }
      }
    Some({ status: ImageFailed, .. }) =>
      if sun_image_failed_record_can_retry(source) {
        self.mark_sun_image_loading_if_needed(source)
        None
      } else {
        None
      }
    _ => {
      self.mark_sun_image_loading_if_needed(source)
      None
    }
  }
}

///|
fn SunRasterRenderer::mark_sun_image_loading_if_needed(
  self : SunRasterRenderer,
  source : String,
) -> Unit {
  match self.image_lifecycle.lookup(source) {
    Some({ status: ImageLoading, .. }) => ()
    _ => ignore(self.image_lifecycle.mark_loading(source))
  }
}

///|
fn SunRasterRenderer::mark_sun_image_ready_if_needed(
  self : SunRasterRenderer,
  source : String,
  pixmap : @sun_graphics.Pixmap,
) -> Unit {
  if sun_image_record_needs_ready_update(
      self.image_lifecycle.lookup(source),
      pixmap.get_width(),
      pixmap.get_height(),
    ) {
    ignore(
      self.image_lifecycle.mark_ready(
        source,
        width=pixmap.get_width(),
        height=pixmap.get_height(),
      ),
    )
  }
}

///|
fn SunRasterRenderer::mark_sun_image_failed_if_needed(
  self : SunRasterRenderer,
  source : String,
  diagnostic : String,
) -> Unit {
  if sun_image_record_needs_failed_update(
      self.image_lifecycle.lookup(source),
      diagnostic,
    ) {
    ignore(self.image_lifecycle.mark_failed(source, diagnostic~))
  }
}

///|
fn SunRasterRenderer::draw_image_pixmap(
  self : SunRasterRenderer,
  image : @core.ImageRun,
  pixmap : @sun_graphics.Pixmap,
  opacity : Double,
) -> Unit {
  let placement = sun_image_placement(
    image.frame,
    @core.Size::new(
      width=pixmap.get_width().to_double(),
      height=pixmap.get_height().to_double(),
    ),
    image.fit,
  )
  let paint = @sun_graphics.Paint::new().set_opacity(image.opacity * opacity)
  let dst_x = sun_dim_to_int(placement.frame.origin.x)
  let dst_y = sun_dim_to_int(placement.frame.origin.y)
  let dst_w = sun_dim_to_int(placement.frame.size.width)
  let dst_h = sun_dim_to_int(placement.frame.size.height)
  guard dst_w > 0 && dst_h > 0 else { return }
  match placement.src_rect {
    Some(src_rect) =>
      self.canvas.draw_quality_pixmap_rect(
        pixmap,
        src_rect,
        dst_x,
        dst_y,
        dst_w,
        dst_h,
        paint,
        Balanced,
      )
    None =>
      self.canvas.draw_quality_pixmap(
        pixmap,
        dst_x,
        dst_y,
        dst_w,
        dst_h,
        paint,
        Balanced,
      )
  }
}

///|
/// Placeholder for an image that is not on screen yet. Same design as the Skia
/// renderer's (see `moui_skia_renderer/renderer_image.mbt`): a near-transparent
/// neutral card with a hairline border and a centered "picture" glyph, instead
/// of a full-frame opaque gradient. Only the primitives differ — this canvas
/// paints through `@sun_graphics`, which has its own rounded-rect, circle,
/// polygon and line calls.
fn SunRasterRenderer::draw_image_placeholder(
  self : SunRasterRenderer,
  image : @core.ImageRun,
  opacity : Double,
) -> Unit {
  let frame = image.frame
  guard frame.size.width > 0.0 && frame.size.height > 0.0 else { return }
  let failed = self.sun_image_placeholder_failed(image.source)
  let radius = sun_image_placeholder_radius(frame)
  self.canvas.fill_rounded_rect(
    frame.origin.x,
    frame.origin.y,
    frame.size.width,
    frame.size.height,
    radius,
    sun_brush_paint(sun_image_placeholder_fill(image), opacity),
  )
  self.canvas.stroke_rounded_rect(
    frame.origin.x,
    frame.origin.y,
    frame.size.width,
    frame.size.height,
    radius,
    sun_brush_paint(sun_image_placeholder_border(image, failed), opacity).set_stroke_width(
      sun_image_placeholder_border_width,
    ),
  )
  match sun_image_placeholder_glyph_box(frame) {
    Some(box) => self.draw_image_placeholder_glyph(box, image, opacity, failed)
    None => ()
  }
}

///|
/// Card corner radius, clamped so a box shorter than the radius still gets a
/// sensible quarter-round instead of an over-rounded pill.
let sun_image_placeholder_radius_max : Double = 8.0

///|
fn sun_image_placeholder_radius(frame : @core.Rect) -> Double {
  let half = sun_image_placeholder_min(frame.size.width, frame.size.height) /
    2.0
  if half < sun_image_placeholder_radius_max {
    half
  } else {
    sun_image_placeholder_radius_max
  }
}

///|
fn sun_image_placeholder_min(a : Double, b : Double) -> Double {
  if a < b {
    a
  } else {
    b
  }
}

///|
fn sun_image_placeholder_max(a : Double, b : Double) -> Double {
  if a > b {
    a
  } else {
    b
  }
}

///|
let sun_image_placeholder_border_width : Double = 1.0

///|
/// Shortest side that still fits a legible glyph; below this the card stands
/// alone (badges and inline thumbnails are too small to draw into).
let sun_image_placeholder_glyph_min_side : Double = 48.0

///|
let sun_image_placeholder_glyph_stroke : Double = 1.5

///|
fn sun_image_placeholder_fill(run : @core.ImageRun) -> @core.Brush {
  @core.Brush::solid(
    @core.Color::rgba(r=0.42, g=0.47, b=0.55, a=0.07 * run.opacity),
  )
}

///|
fn sun_image_placeholder_border(
  run : @core.ImageRun,
  failed : Bool,
) -> @core.Brush {
  let alpha = if failed { 0.34 } else { 0.18 }
  @core.Brush::solid(
    @core.Color::rgba(r=0.42, g=0.47, b=0.55, a=alpha * run.opacity),
  )
}

///|
fn sun_image_placeholder_glyph_brush(
  run : @core.ImageRun,
  failed : Bool,
) -> @core.Brush {
  let alpha = if failed { 0.62 } else { 0.45 }
  @core.Brush::solid(
    @core.Color::rgba(r=0.38, g=0.44, b=0.54, a=alpha * run.opacity),
  )
}

///|
/// True when the source is known to have failed, read from the same lifecycle
/// record `sun_image_for_source` writes.
fn SunRasterRenderer::sun_image_placeholder_failed(
  self : SunRasterRenderer,
  source : String,
) -> Bool {
  match self.image_lifecycle.lookup(source) {
    Some({ status: ImageFailed, .. }) => true
    _ => false
  }
}

///|
/// Square glyph box centered in the card, or `None` when the card is too small.
fn sun_image_placeholder_glyph_box(frame : @core.Rect) -> @core.Rect? {
  let short = sun_image_placeholder_min(frame.size.width, frame.size.height)
  if short < sun_image_placeholder_glyph_min_side {
    return None
  }
  let side = sun_image_placeholder_max(
    short * 0.34,
    sun_image_placeholder_glyph_min_side * 0.34,
  )
  let side = sun_image_placeholder_min(side, 40.0)
  Some(
    @core.Rect::new(
      x=frame.origin.x + (frame.size.width - side) / 2.0,
      y=frame.origin.y + (frame.size.height - side) / 2.0,
      width=side,
      height=side,
    ),
  )
}

///|
/// The "picture" glyph: rounded frame, sun, peak, plus a slash when the load
/// failed. Fractions of `box`, so one geometry serves every size.
fn SunRasterRenderer::draw_image_placeholder_glyph(
  self : SunRasterRenderer,
  box : @core.Rect,
  run : @core.ImageRun,
  opacity : Double,
  failed : Bool,
) -> Unit {
  let paint = sun_brush_paint(
    sun_image_placeholder_glyph_brush(run, failed),
    opacity,
  )
  let stroke = sun_image_placeholder_glyph_stroke
  let at = (fx : Double, fy : Double) => {
    @sun_graphics.Point::new(
      x=box.origin.x + box.size.width * fx,
      y=box.origin.y + box.size.height * fy,
    )
  }
  let side = sun_image_placeholder_min(box.size.width, box.size.height)
  let inset = stroke / 2.0
  self.canvas.stroke_rounded_rect(
    box.origin.x + inset,
    box.origin.y + inset,
    box.size.width - stroke,
    box.size.height - stroke,
    side * 0.18,
    paint.set_stroke_width(stroke),
  )
  let sun_center = at(0.33, 0.34)
  self.canvas.fill_circle(sun_center.x, sun_center.y, side * 0.085, paint)
  self.canvas.fill_polygon(
    [at(0.16, 0.76), at(0.45, 0.44), at(0.74, 0.76)],
    paint,
  )
  if failed {
    // Slash across the frame, past both corners so it cannot be mistaken for
    // part of the picture.
    let from = at(0.06, 0.06)
    let to = at(0.94, 0.94)
    self.canvas.stroke_line(
      from.x,
      from.y,
      to.x,
      to.y,
      paint.set_stroke_width(stroke),
    )
  }
}

///|
fn SunRasterRenderer::decode_image_source_with_cache(
  self : SunRasterRenderer,
  source : String,
) -> Result[@sun_graphics.Pixmap, String] {
  sun_decode_image_source_with_cache(self.resources.get_image_cache(), source)
}

///|
fn sun_decode_image_source_with_cache(
  cache : @sun_graphics.PixmapCache,
  source : String,
) -> Result[@sun_graphics.Pixmap, String] {
  match cache.get(source) {
    Some(pixmap) => Ok(pixmap)
    None =>
      match sun_image_source_bytes(source) {
        Some({ format, data, }) =>
          sun_decode_image_data_with_fallback(
            cache,
            source,
            data,
            Some(format),
            "source",
            "Sun renderer could not decode image source: \{source}",
          )
        None => Err("Sun renderer could not decode image source: \{source}")
      }
  }
}

///|
fn sun_decode_image_bytes_with_cache(
  cache : @sun_graphics.PixmapCache,
  source : String,
  data : Bytes,
) -> Result[@sun_graphics.Pixmap, String] {
  sun_decode_image_data_with_fallback(
    cache,
    source,
    data,
    sun_image_format_for_source(source, sun_image_media_type(source)),
    "bytes",
    "Sun renderer could not infer image format: \{source}",
  )
}

///|
fn sun_decode_image_data_with_fallback(
  cache : @sun_graphics.PixmapCache,
  source : String,
  data : Bytes,
  source_format : SunImageFormat?,
  failure_kind : String,
  infer_diagnostic : String,
) -> Result[@sun_graphics.Pixmap, String] {
  let byte_format = sun_image_format_for_bytes(data)
  let format = match source_format {
    Some(format) => Some(format)
    None => byte_format
  }
  match format {
    Some(format) =>
      match
        sun_decode_image_data_as_format(
          cache, source, data, format, failure_kind,
        ) {
        Ok(pixmap) => Ok(pixmap)
        Err(diagnostic) =>
          match byte_format {
            Some(fallback_format) =>
              if !sun_image_format_is_same(fallback_format, format) {
                sun_decode_image_data_as_format(
                  cache, source, data, fallback_format, failure_kind,
                )
              } else {
                Err(diagnostic)
              }
            None => Err(diagnostic)
          }
      }
    None => Err(infer_diagnostic)
  }
}

///|
fn sun_decode_image_data_as_format(
  cache : @sun_graphics.PixmapCache,
  source : String,
  data : Bytes,
  format : SunImageFormat,
  failure_kind : String,
) -> Result[@sun_graphics.Pixmap, String] {
  match format {
    Png =>
      match cache.get_or_decode_png_result(source, data) {
        Ok(result) => Ok(result.get_pixmap())
        Err(_) =>
          Err(
            "Sun renderer could not decode PNG image \{failure_kind}: \{source}",
          )
      }
    Bmp =>
      match cache.get_or_decode_bmp_result(source, data) {
        Ok(result) => Ok(result.get_pixmap())
        Err(_) =>
          Err(
            "Sun renderer could not decode BMP image \{failure_kind}: \{source}",
          )
      }
    Jpeg =>
      sun_decode_jpeg_image_with_cache(
        cache,
        source,
        data,
        failure_kind="bytes",
      )
  }
}

///|
fn sun_decode_jpeg_image_with_cache(
  cache : @sun_graphics.PixmapCache,
  source : String,
  data : Bytes,
  failure_kind~ : String,
) -> Result[@sun_graphics.Pixmap, String] {
  match cache.get(source) {
    Some(pixmap) => Ok(pixmap)
    None => {
      let decoded = @image.decode_jpeg(data) catch {
        _ =>
          return Err(
            "Sun renderer could not decode JPEG image \{failure_kind}: \{source}",
          )
      }
      match
        cache.get_or_decode_rgba_result(
          source,
          decoded.width,
          decoded.height,
          decoded.data,
        ) {
        Ok(result) => Ok(result.get_pixmap())
        Err(_) =>
          Err(
            "Sun renderer could not decode JPEG image \{failure_kind}: \{source}",
          )
      }
    }
  }
}

///|
pub fn sun_image_load_completion(
  source : String,
) -> @render.ImageResourceLoadCompletion {
  match sun_image_source_bytes(source) {
    Some({ data, .. }) => sun_image_load_completion_from_bytes(source, data)
    None =>
      @render.ImageResourceLoadCompletion::failed(
        source,
        diagnostic="Sun renderer could not decode image source: \{source}",
      )
  }
}

///|
pub fn sun_image_load_completion_from_bytes(
  source : String,
  data : Bytes,
  background_io? : Bool = false,
) -> @render.ImageResourceLoadCompletion {
  let cache = @sun_graphics.PixmapCache::with_capacity(1)
  match sun_decode_image_bytes_with_cache(cache, source, data) {
    Ok(pixmap) =>
      @render.ImageResourceLoadCompletion::ready_decoded_rgba(
        source,
        width=pixmap.get_width(),
        height=pixmap.get_height(),
        row_bytes=pixmap.get_width() * 4,
        pixmap.to_rgba_bytes(),
        background_io~,
      )
    Err(diagnostic) =>
      @render.ImageResourceLoadCompletion::failed(
        source,
        diagnostic~,
        background_io~,
      )
  }
}

///|
priv enum SunImageFormat {
  Png
  Bmp
  Jpeg
}

///|
priv struct SunImageSourceBytes {
  format : SunImageFormat
  data : Bytes
}

///|
priv struct SunImagePlacement {
  frame : @core.Rect
  src_rect : @sun_graphics.PixelRect?
}

///|
fn sun_image_source_bytes(source : String) -> SunImageSourceBytes? {
  if source.has_prefix("data:") {
    sun_parse_data_uri(source)
  } else {
    None
  }
}

///|
fn sun_image_failed_record_can_retry(source : String) -> Bool {
  let _ = source
  false
}

///|
fn sun_parse_data_uri(uri : String) -> SunImageSourceBytes? {
  let semicolon_pos = uri.find(";")
  let comma_pos = uri.find(",")
  guard semicolon_pos is Some(semi_idx) else { return None }
  guard comma_pos is Some(comma_idx) else { return None }
  guard comma_idx > semi_idx else { return None }
  let chars = uri.to_array()
  let meta = sun_image_media_type(uri)
  let encoding = String::from_array(chars[semi_idx + 1:comma_idx])
  guard encoding == "base64" else { return None }
  let data_part = String::from_array(chars[comma_idx + 1:])
  let decoded : Bytes? = Some(@base64.decode(data_part, ignore_whitespace=true)) catch {
    _ => None
  }
  match decoded {
    Some(data) =>
      match sun_image_format_for_source_or_bytes("", meta, data) {
        Some(format) => Some({ format, data, })
        None => None
      }
    None => None
  }
}

///|
fn sun_image_media_type(source : String) -> String {
  guard source.has_prefix("data:") else { return "" }
  let semicolon_pos = source.find(";")
  guard semicolon_pos is Some(semi_idx) else { return "" }
  let chars = source.to_array()
  String::from_array(chars[5:semi_idx])
}

///|
fn sun_image_format_for_source(
  source : String,
  media_type : String,
) -> SunImageFormat? {
  let normalized_media_type = media_type.to_lower()
  let normalized_source = source.to_lower()
  if normalized_media_type.contains("png") ||
    normalized_source.has_suffix(".png") {
    Some(Png)
  } else if normalized_media_type.contains("bmp") ||
    normalized_source.has_suffix(".bmp") {
    Some(Bmp)
  } else if normalized_media_type.contains("jpeg") ||
    normalized_media_type.contains("jpg") ||
    normalized_source.has_suffix(".jpg") ||
    normalized_source.has_suffix(".jpeg") {
    Some(Jpeg)
  } else {
    None
  }
}

///|
fn sun_image_format_for_source_or_bytes(
  source : String,
  media_type : String,
  data : Bytes,
) -> SunImageFormat? {
  match sun_image_format_for_source(source, media_type) {
    Some(format) => Some(format)
    None => sun_image_format_for_bytes(data)
  }
}

///|
fn sun_image_format_is_same(
  left : SunImageFormat,
  right : SunImageFormat,
) -> Bool {
  match left {
    Png =>
      match right {
        Png => true
        _ => false
      }
    Bmp =>
      match right {
        Bmp => true
        _ => false
      }
    Jpeg =>
      match right {
        Jpeg => true
        _ => false
      }
  }
}

///|
fn sun_image_format_for_bytes(data : Bytes) -> SunImageFormat? {
  if data.length() >= 8 &&
    data[0] == 137 &&
    data[1] == 80 &&
    data[2] == 78 &&
    data[3] == 71 &&
    data[4] == 13 &&
    data[5] == 10 &&
    data[6] == 26 &&
    data[7] == 10 {
    Some(Png)
  } else if data.length() >= 2 && data[0] == 66 && data[1] == 77 {
    Some(Bmp)
  } else if data.length() >= 2 && data[0] == 255 && data[1] == 216 {
    Some(Jpeg)
  } else {
    None
  }
}

///|
fn sun_image_placement(
  frame : @core.Rect,
  image_size : @core.Size,
  fit : @core.ImageFit,
) -> SunImagePlacement {
  let placement = @render_common.image_placement(frame, image_size, fit)
  match placement.crop_axis {
    None => { frame: placement.frame, src_rect: None, }
    Horizontal => {
      let crop_width = (image_size.width * placement.visible).round().to_int()
      let left = ((image_size.width - crop_width.to_double()) * 0.5)
        .round()
        .to_int()
      {
        frame: placement.frame,
        src_rect: Some(
          @sun_graphics.PixelRect::new(
            x=left,
            y=0,
            width=crop_width,
            height=image_size.height.round().to_int(),
          ),
        ),
      }
    }
    Vertical => {
      let crop_height = (image_size.height * placement.visible).round().to_int()
      let top = ((image_size.height - crop_height.to_double()) * 0.5)
        .round()
        .to_int()
      {
        frame: placement.frame,
        src_rect: Some(
          @sun_graphics.PixelRect::new(
            x=0,
            y=top,
            width=image_size.width.round().to_int(),
            height=crop_height,
          ),
        ),
      }
    }
  }
}

///|
fn sun_image_record_needs_ready_update(
  record : @render.ImageResourceRecord?,
  width : Int,
  height : Int,
) -> Bool {
  match record {
    Some(existing) =>
      existing.status != ImageReady ||
      existing.width != width ||
      existing.height != height
    None => true
  }
}

///|
fn sun_image_record_needs_failed_update(
  record : @render.ImageResourceRecord?,
  diagnostic : String,
) -> Bool {
  match record {
    Some(existing) =>
      existing.status != ImageFailed || existing.diagnostic != diagnostic
    None => true
  }
}

///|