///|
/// Copy and validate a matrix of already validated RGBA pixels.
fn copy_color_image(
  data : Array[Array[@colors.Rgba]],
) -> Array[Array[@colors.Rgba]] raise @core.DataError {
  if data.length() == 0 || data[0].length() == 0 {
    raise @core.DataError::InvalidImage(
      message="color image must have at least one cell",
    )
  }
  let columns = data[0].length()
  let result = Array(capacity=data.length())
  for row in data {
    if row.length() != columns {
      raise @core.DataError::InvalidImage(
        message="color image rows must have equal length",
      )
    }
    result.push(row.copy())
  }
  result
}

///|
/// Convert a raw pixel channel vector to an RGBA value. NaN and infinite
/// channels follow Matplotlib's invalid-pixel rule and produce a fully
/// transparent pixel; finite channels are clipped to [0, 1].
fn color_pixel_from_channels(
  channels : Array[Double],
  expected : Int32,
) -> @colors.Rgba raise @core.DataError {
  if channels.length() != expected {
    raise @core.DataError::InvalidImage(
      message="color image pixels must have \{expected} channels",
    )
  }
  let mut has_nan = false
  for value in channels {
    if value.is_nan() {
      has_nan = true
    } else if value.is_inf() {
      has_nan = true
    }
  }
  if has_nan {
    try! @colors.rgba(0.0, 0.0, 0.0, alpha=0.0)
  } else if expected == 3 {
    try! @colors.rgba(
      clip_color_channel(channels[0]),
      clip_color_channel(channels[1]),
      clip_color_channel(channels[2]),
    )
  } else {
    try! @colors.rgba(
      clip_color_channel(channels[0]),
      clip_color_channel(channels[1]),
      clip_color_channel(channels[2]),
      alpha=clip_color_channel(channels[3]),
    )
  }
}

///|
fn clip_color_channel(value : Double) -> Double {
  if value < 0.0 {
    0.0
  } else if value > 1.0 {
    1.0
  } else {
    value
  }
}

///|
fn color_image_from_channels(
  data : Array[Array[Array[Double]]],
  expected : Int32,
) -> Array[Array[@colors.Rgba]] raise @core.DataError {
  if data.length() == 0 || data[0].length() == 0 {
    raise @core.DataError::InvalidImage(
      message="color image must have at least one cell",
    )
  }
  let columns = data[0].length()
  let result = Array(capacity=data.length())
  for row in data {
    if row.length() != columns {
      raise @core.DataError::InvalidImage(
        message="color image rows must have equal length",
      )
    }
    let pixels = Array(capacity=columns)
    for channels in row {
      pixels.push(color_pixel_from_channels(channels, expected))
    }
    result.push(pixels)
  }
  result
}

///|
fn color_image_from_rgb(
  data : Array[Array[@colors.Rgb]],
) -> Array[Array[@colors.Rgba]] raise @core.DataError {
  if data.length() == 0 || data[0].length() == 0 {
    raise @core.DataError::InvalidImage(
      message="color image must have at least one cell",
    )
  }
  let columns = data[0].length()
  let result = Array(capacity=data.length())
  for row in data {
    if row.length() != columns {
      raise @core.DataError::InvalidImage(
        message="color image rows must have equal length",
      )
    }
    let pixels = Array(capacity=columns)
    for pixel in row {
      pixels.push(pixel.to_rgba())
    }
    result.push(pixels)
  }
  result
}

///|
fn Axes::add_color_image(
  self : Axes,
  data : Array[Array[@colors.Rgba]],
  origin~ : String,
  extent~ : Array[Double],
  aspect~ : String,
  alpha~ : Double,
  interpolation~ : String,
) -> Unit raise @core.DataError {
  let pixels = copy_color_image(data)
  validate_image_alpha(alpha)
  let image_origin = parse_image_origin(origin)
  let image_interpolation = parse_image_interpolation(interpolation)
  let image_extent = parse_image_extent(extent)
  self.apply_image_aspect(aspect)
  self.attach_image_checked(
    Artist::ColorImage(
      pixels, image_origin, image_extent, alpha, image_interpolation,
    ),
    pixels.length(),
    pixels[0].length(),
    image_origin,
    image_extent,
  )
}

///|
/// Add a prevalidated RGBA image. Pixels are placed with the same extent,
/// origin, aspect, and shared-axis behavior as scalar `imshow`.
pub fn Figure::imshow_rgba(
  self : Figure,
  data : Array[Array[@colors.Rgba]],
  origin~ : String = "upper",
  extent~ : Array[Double] = [],
  aspect~ : String = "equal",
  alpha~ : Double = 1.0,
  interpolation~ : String = "nearest",
) -> Unit raise @core.DataError {
  self
  .axes()
  .add_color_image(data, origin~, extent~, aspect~, alpha~, interpolation~)
}

///|
/// Add an opaque RGB image. Alpha is filled with one before it enters the
/// backend-neutral scene model.
pub fn Figure::imshow_rgb(
  self : Figure,
  data : Array[Array[@colors.Rgb]],
  origin~ : String = "upper",
  extent~ : Array[Double] = [],
  aspect~ : String = "equal",
  alpha~ : Double = 1.0,
  interpolation~ : String = "nearest",
) -> Unit raise @core.DataError {
  self.imshow_rgba(
    color_image_from_rgb(data),
    origin~,
    extent~,
    aspect~,
    alpha~,
    interpolation~,
  )
}

///|
/// Add an RGBA image from raw normalized four-channel pixels.
pub fn Figure::imshow_rgba_channels(
  self : Figure,
  data : Array[Array[Array[Double]]],
  origin~ : String = "upper",
  extent~ : Array[Double] = [],
  aspect~ : String = "equal",
  alpha~ : Double = 1.0,
  interpolation~ : String = "nearest",
) -> Unit raise @core.DataError {
  self.imshow_rgba(
    color_image_from_channels(data, 4),
    origin~,
    extent~,
    aspect~,
    alpha~,
    interpolation~,
  )
}

///|
/// Add an RGB image from raw normalized three-channel pixels.
pub fn Figure::imshow_rgb_channels(
  self : Figure,
  data : Array[Array[Array[Double]]],
  origin~ : String = "upper",
  extent~ : Array[Double] = [],
  aspect~ : String = "equal",
  alpha~ : Double = 1.0,
  interpolation~ : String = "nearest",
) -> Unit raise @core.DataError {
  self.imshow_rgba(
    color_image_from_channels(data, 3),
    origin~,
    extent~,
    aspect~,
    alpha~,
    interpolation~,
  )
}

///|
/// Alias for the typed RGBA entry point.
pub fn Figure::imshow_color(
  self : Figure,
  data : Array[Array[@colors.Rgba]],
  origin~ : String = "upper",
  extent~ : Array[Double] = [],
  aspect~ : String = "equal",
  alpha~ : Double = 1.0,
  interpolation~ : String = "nearest",
) -> Unit raise @core.DataError {
  self.imshow_rgba(data, origin~, extent~, aspect~, alpha~, interpolation~)
}

///|
/// Add a prevalidated RGBA image through an axes handle.
pub fn Axes::imshow_rgba(
  self : Axes,
  data : Array[Array[@colors.Rgba]],
  origin~ : String = "upper",
  extent~ : Array[Double] = [],
  aspect~ : String = "equal",
  alpha~ : Double = 1.0,
  interpolation~ : String = "nearest",
) -> Unit raise @core.DataError {
  self.add_color_image(data, origin~, extent~, aspect~, alpha~, interpolation~)
}

///|
/// Add an opaque RGB image through an axes handle.
pub fn Axes::imshow_rgb(
  self : Axes,
  data : Array[Array[@colors.Rgb]],
  origin~ : String = "upper",
  extent~ : Array[Double] = [],
  aspect~ : String = "equal",
  alpha~ : Double = 1.0,
  interpolation~ : String = "nearest",
) -> Unit raise @core.DataError {
  self.imshow_rgba(
    color_image_from_rgb(data),
    origin~,
    extent~,
    aspect~,
    alpha~,
    interpolation~,
  )
}

///|
/// Add an RGBA image from raw normalized four-channel pixels through axes.
pub fn Axes::imshow_rgba_channels(
  self : Axes,
  data : Array[Array[Array[Double]]],
  origin~ : String = "upper",
  extent~ : Array[Double] = [],
  aspect~ : String = "equal",
  alpha~ : Double = 1.0,
  interpolation~ : String = "nearest",
) -> Unit raise @core.DataError {
  self.imshow_rgba(
    color_image_from_channels(data, 4),
    origin~,
    extent~,
    aspect~,
    alpha~,
    interpolation~,
  )
}

///|
/// Add an RGB image from raw normalized three-channel pixels through axes.
pub fn Axes::imshow_rgb_channels(
  self : Axes,
  data : Array[Array[Array[Double]]],
  origin~ : String = "upper",
  extent~ : Array[Double] = [],
  aspect~ : String = "equal",
  alpha~ : Double = 1.0,
  interpolation~ : String = "nearest",
) -> Unit raise @core.DataError {
  self.imshow_rgba(
    color_image_from_channels(data, 3),
    origin~,
    extent~,
    aspect~,
    alpha~,
    interpolation~,
  )
}

///|
/// Alias for the typed RGBA axes entry point.
pub fn Axes::imshow_color(
  self : Axes,
  data : Array[Array[@colors.Rgba]],
  origin~ : String = "upper",
  extent~ : Array[Double] = [],
  aspect~ : String = "equal",
  alpha~ : Double = 1.0,
  interpolation~ : String = "nearest",
) -> Unit raise @core.DataError {
  self.imshow_rgba(data, origin~, extent~, aspect~, alpha~, interpolation~)
}