///|
/// The vertical orientation used when mapping matrix rows to data space.
pub(all) enum ImageOrigin {
  Upper
  Lower
}

///|
/// The sampling policy used when an image is mapped to the SVG display area.
///
/// `Nearest` draws source cells as vector rectangles. `NoInterpolation` embeds
/// source pixels in a PNG on linear axes; nonlinear axes use the same cell
/// geometry as `Nearest`. The remaining modes use a display-sized PNG on
/// linear axes and fall back to source-cell geometry on nonlinear axes.
pub(all) enum ImageInterpolation {
  Nearest
  NoInterpolation
  Bilinear
  Bicubic
  Spline16
  Spline36
  Hanning
  Hamming
  Hermite
  Kaiser
  Quadric
  Catrom
  Gaussian
  Bessel
  Mitchell
  Sinc
  Lanczos
  Blackman
  Auto
  Antialiased
}

///|
fn parse_image_origin(value : String) -> ImageOrigin raise @core.DataError {
  match value {
    "upper" => ImageOrigin::Upper
    "lower" => ImageOrigin::Lower
    _ =>
      raise @core.DataError::InvalidImage(
        message="image origin must be 'upper' or 'lower'",
      )
  }
}

///|
fn parse_image_interpolation(
  value : String,
) -> ImageInterpolation raise @core.DataError {
  match value.to_lower() {
    "nearest" => ImageInterpolation::Nearest
    "none" => ImageInterpolation::NoInterpolation
    "bilinear" => ImageInterpolation::Bilinear
    "bicubic" => ImageInterpolation::Bicubic
    "spline16" => ImageInterpolation::Spline16
    "spline36" => ImageInterpolation::Spline36
    "hanning" => ImageInterpolation::Hanning
    "hamming" => ImageInterpolation::Hamming
    "hermite" => ImageInterpolation::Hermite
    "kaiser" => ImageInterpolation::Kaiser
    "quadric" => ImageInterpolation::Quadric
    "catrom" => ImageInterpolation::Catrom
    "gaussian" => ImageInterpolation::Gaussian
    "bessel" => ImageInterpolation::Bessel
    "mitchell" => ImageInterpolation::Mitchell
    "sinc" => ImageInterpolation::Sinc
    "lanczos" => ImageInterpolation::Lanczos
    "blackman" => ImageInterpolation::Blackman
    "auto" => ImageInterpolation::Auto
    "antialiased" => ImageInterpolation::Antialiased
    _ =>
      raise @core.DataError::InvalidImage(
        message="image interpolation supports 'nearest', 'none', 'bilinear', 'bicubic', 'spline16', 'spline36', 'hanning', 'hamming', 'hermite', 'kaiser', 'quadric', 'catrom', 'gaussian', 'bessel', 'mitchell', 'sinc', 'lanczos', 'blackman', 'auto', and 'antialiased'",
      )
  }
}

///|
fn parse_image_extent(
  values : Array[Double],
) -> (Double, Double, Double, Double)? raise @core.DataError {
  if values.length() == 0 {
    return None
  }
  if values.length() != 4 {
    raise @core.DataError::InvalidImage(
      message="image extent must contain four values",
    )
  }
  let left = values[0]
  let right = values[1]
  let bottom = values[2]
  let top = values[3]
  if left.is_nan() ||
    left.is_inf() ||
    right.is_nan() ||
    right.is_inf() ||
    bottom.is_nan() ||
    bottom.is_inf() ||
    top.is_nan() ||
    top.is_inf() {
    raise @core.DataError::InvalidImage(message="image extent must be finite")
  }
  if left == right || bottom == top {
    raise @core.DataError::InvalidImage(
      message="image extent bounds must be distinct",
    )
  }
  Some((left, right, bottom, top))
}

///|
fn Axes::apply_image_aspect(
  self : Axes,
  value : String,
) -> Unit raise @core.DataError {
  match value {
    "equal" => self.set_aspect_equal()
    "auto" => self.set_aspect_auto()
    _ =>
      raise @core.DataError::InvalidImage(
        message="image aspect must be 'equal' or 'auto'",
      )
  }
}

///|
/// Validate the global opacity applied to an image artist.
fn validate_image_alpha(alpha : Double) -> Unit raise @core.DataError {
  if alpha.is_nan() || alpha.is_inf() || alpha < 0.0 || alpha > 1.0 {
    raise @core.DataError::InvalidImage(
      message="image alpha must be finite and between 0 and 1",
    )
  }
}

///|
fn copy_image_data(data : Array[Array[Double]]) -> Array[Array[Double]] {
  let result = Array(capacity=data.length())
  for row in data {
    result.push(row.copy())
  }
  result
}

///|
fn validate_image_data(
  data : Array[Array[Double]],
  vmin : Double?,
  vmax : Double?,
) -> Unit raise @core.DataError {
  if data.length() == 0 || data[0].length() == 0 {
    raise @core.DataError::InvalidImage(
      message="image must have at least one cell",
    )
  }
  let columns = data[0].length()
  for row in data {
    if row.length() != columns {
      raise @core.DataError::InvalidImage(
        message="image rows must have equal length",
      )
    }
  }
  match vmin {
    Some(value) =>
      if value.is_nan() || value.is_inf() {
        raise @core.DataError::InvalidImage(message="vmin must be finite")
      }
    None => ()
  }
  match vmax {
    Some(value) =>
      if value.is_nan() || value.is_inf() {
        raise @core.DataError::InvalidImage(message="vmax must be finite")
      }
    None => ()
  }
}

///|
fn resolve_colormap(name : String) -> @colors.ColorMap raise @core.DataError {
  match @colors.find_colormap(name) {
    Some(value) => value
    None =>
      raise @core.DataError::InvalidImage(message="unknown colormap: \{name}")
  }
}

///|
fn linear_color_norm(
  vmin : Double?,
  vmax : Double?,
) -> @colors.ColorNorm raise @core.DataError {
  try {
    let norm = match (vmin, vmax) {
      (Some(lower), Some(upper)) => @colors.normalize(vmin=lower, vmax=upper)
      (Some(lower), None) => @colors.normalize(vmin=lower)
      (None, Some(upper)) => @colors.normalize(vmax=upper)
      (None, None) => @colors.normalize()
    }
    @colors.linear_color_norm(norm)
  } catch {
    @colors.NormalizeError::InvalidBounds(message~)
    | @colors.NormalizeError::InvalidBase(message~)
    | @colors.NormalizeError::InvalidValue(message~) =>
      raise @core.DataError::InvalidImage(message~)
    @colors.NormalizeError::NoFiniteData
    | @colors.NormalizeError::NotScaled
    | @colors.NormalizeError::NotInvertible =>
      raise @core.DataError::InvalidImage(message="invalid image normalization")
  }
}

///|
fn Axes::add_image(
  self : Axes,
  data : Array[Array[Double]],
  vmin : Double?,
  vmax : Double?,
  cmap : @colors.ColorMap,
  origin~ : String,
  extent~ : Array[Double],
  aspect~ : String,
  alpha~ : Double,
  interpolation~ : String,
) -> Unit raise @core.DataError {
  validate_image_data(data, vmin, vmax)
  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)
  let norm = linear_color_norm(vmin, vmax)
  self.apply_image_aspect(aspect)
  self.attach_image_checked(
    Artist::ImageNorm(
      copy_image_data(data),
      norm,
      cmap,
      image_origin,
      image_extent,
      alpha,
      image_interpolation,
    ),
    data.length(),
    data[0].length(),
    image_origin,
    image_extent,
  )
}

///|
fn Axes::add_image_norm(
  self : Axes,
  data : Array[Array[Double]],
  norm : @colors.ColorNorm,
  cmap : @colors.ColorMap,
  origin~ : String,
  extent~ : Array[Double],
  aspect~ : String,
  alpha~ : Double,
  interpolation~ : String,
) -> Unit raise @core.DataError {
  validate_image_data(data, norm.vmin(), norm.vmax())
  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::ImageNorm(
      copy_image_data(data),
      norm,
      cmap,
      image_origin,
      image_extent,
      alpha,
      image_interpolation,
    ),
    data.length(),
    data[0].length(),
    image_origin,
    image_extent,
  )
}

///|
/// Add a two-dimensional scalar image. Non-finite cells use the colormap's
/// bad color (transparent by default). `interpolation` accepts the Matplotlib
/// filter names `"nearest"` (the default), `"none"`, `"bilinear"`,
/// `"bicubic"`, `"spline16"`, `"spline36"`, `"hanning"`, `"hamming"`,
/// `"hermite"`, `"kaiser"`, `"quadric"`, `"catrom"`, `"gaussian"`,
/// `"bessel"`, `"mitchell"`, `"sinc"`, `"lanczos"`, `"blackman"`,
/// `"auto"`, and `"antialiased"`, ignoring case. On linear axes, `"none"`
/// embeds original-resolution pixels, while the filtered modes rasterize to
/// the display size before embedding the PNG. `"auto"` and `"antialiased"`
/// choose nearest for integer or large upscales and Hanning otherwise.
///
/// ```mbt check
/// test {
///   let fig = @scene.figure()
///   fig.imshow([[0.0, 1.0]], interpolation="none")
///   assert_true(fig.to_svg().contains("data:image/png;base64,"))
/// }
/// ```
pub fn Figure::imshow(
  self : Figure,
  data : Array[Array[Double]],
  vmin? : Double,
  vmax? : Double,
  cmap~ : String = "gray",
  origin~ : String = "upper",
  extent~ : Array[Double] = [],
  aspect~ : String = "equal",
  alpha~ : Double = 1.0,
  interpolation~ : String = "nearest",
) -> Unit raise @core.DataError {
  self
  .axes()
  .add_image(
    data,
    vmin,
    vmax,
    resolve_colormap(cmap),
    origin~,
    extent~,
    aspect~,
    alpha~,
    interpolation~,
  )
}

///|
/// Add a scalar image using a custom or builtin colormap object.
pub fn Figure::imshow_with_colormap(
  self : Figure,
  data : Array[Array[Double]],
  cmap : @colors.ColorMap,
  vmin? : Double,
  vmax? : Double,
  origin~ : String = "upper",
  extent~ : Array[Double] = [],
  aspect~ : String = "equal",
  alpha~ : Double = 1.0,
  interpolation~ : String = "nearest",
) -> Unit raise @core.DataError {
  self
  .axes()
  .add_image(
    data,
    vmin,
    vmax,
    cmap,
    origin~,
    extent~,
    aspect~,
    alpha~,
    interpolation~,
  )
}

///|
/// Add a scalar image with an explicit linear or logarithmic normalization.
pub fn Figure::imshow_with_norm(
  self : Figure,
  data : Array[Array[Double]],
  norm : @colors.ColorNorm,
  cmap~ : String = "gray",
  origin~ : String = "upper",
  extent~ : Array[Double] = [],
  aspect~ : String = "equal",
  alpha~ : Double = 1.0,
  interpolation~ : String = "nearest",
) -> Unit raise @core.DataError {
  self
  .axes()
  .add_image_norm(
    data,
    norm,
    resolve_colormap(cmap),
    origin~,
    extent~,
    aspect~,
    alpha~,
    interpolation~,
  )
}

///|
/// Add a scalar image with explicit normalization and colormap objects.
pub fn Figure::imshow_with_norm_and_colormap(
  self : Figure,
  data : Array[Array[Double]],
  norm : @colors.ColorNorm,
  cmap : @colors.ColorMap,
  origin~ : String = "upper",
  extent~ : Array[Double] = [],
  aspect~ : String = "equal",
  alpha~ : Double = 1.0,
  interpolation~ : String = "nearest",
) -> Unit raise @core.DataError {
  self
  .axes()
  .add_image_norm(
    data,
    norm,
    cmap,
    origin~,
    extent~,
    aspect~,
    alpha~,
    interpolation~,
  )
}

///|
/// Add a scalar image through the axes view.
pub fn Axes::imshow(
  self : Axes,
  data : Array[Array[Double]],
  vmin? : Double,
  vmax? : Double,
  cmap~ : String = "gray",
  origin~ : String = "upper",
  extent~ : Array[Double] = [],
  aspect~ : String = "equal",
  alpha~ : Double = 1.0,
  interpolation~ : String = "nearest",
) -> Unit raise @core.DataError {
  self.add_image(
    data,
    vmin,
    vmax,
    resolve_colormap(cmap),
    origin~,
    extent~,
    aspect~,
    alpha~,
    interpolation~,
  )
}

///|
/// Add a scalar image through the axes view using a colormap object.
pub fn Axes::imshow_with_colormap(
  self : Axes,
  data : Array[Array[Double]],
  cmap : @colors.ColorMap,
  vmin? : Double,
  vmax? : Double,
  origin~ : String = "upper",
  extent~ : Array[Double] = [],
  aspect~ : String = "equal",
  alpha~ : Double = 1.0,
  interpolation~ : String = "nearest",
) -> Unit raise @core.DataError {
  self.add_image(
    data,
    vmin,
    vmax,
    cmap,
    origin~,
    extent~,
    aspect~,
    alpha~,
    interpolation~,
  )
}

///|
/// Add a scalar image through the axes view with an explicit normalization.
pub fn Axes::imshow_with_norm(
  self : Axes,
  data : Array[Array[Double]],
  norm : @colors.ColorNorm,
  cmap~ : String = "gray",
  origin~ : String = "upper",
  extent~ : Array[Double] = [],
  aspect~ : String = "equal",
  alpha~ : Double = 1.0,
  interpolation~ : String = "nearest",
) -> Unit raise @core.DataError {
  self.add_image_norm(
    data,
    norm,
    resolve_colormap(cmap),
    origin~,
    extent~,
    aspect~,
    alpha~,
    interpolation~,
  )
}

///|
/// Add a scalar image through the axes view with explicit normalization and
/// colormap objects.
pub fn Axes::imshow_with_norm_and_colormap(
  self : Axes,
  data : Array[Array[Double]],
  norm : @colors.ColorNorm,
  cmap : @colors.ColorMap,
  origin~ : String = "upper",
  extent~ : Array[Double] = [],
  aspect~ : String = "equal",
  alpha~ : Double = 1.0,
  interpolation~ : String = "nearest",
) -> Unit raise @core.DataError {
  self.add_image_norm(
    data,
    norm,
    cmap,
    origin~,
    extent~,
    aspect~,
    alpha~,
    interpolation~,
  )
}