///|
pub(all) enum ToneMap {
  Clamp
  Reinhard
} derive(Debug, Eq)

///|
pub extend ToneMap with @debug.Debug::{to_repr}

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

///|
pub(all) struct PreviewOptions {
  exposure_stops : Double
  gamma : Double
  tone_map : ToneMap
  premultiplied_alpha : Bool
} derive(Debug)

///|
pub extend PreviewOptions with @debug.Debug::{to_repr}

///|
pub fn PreviewOptions::default() -> PreviewOptions {
  {
    exposure_stops: 0.0,
    gamma: 2.2,
    tone_map: Reinhard,
    premultiplied_alpha: false,
  }
}

///|
fn clamp01(v : Double) -> Double {
  if v.is_nan() || v <= 0.0 {
    0.0
  } else if v >= 1.0 {
    1.0
  } else {
    v
  }
}

///|
fn preview_byte(
  value : Double,
  scale : Double,
  options : PreviewOptions,
) -> Byte {
  let v = if value.is_nan() || value <= 0.0 {
    0.0
  } else if value.is_inf() {
    1.0
  } else {
    let exposed = value * scale
    if options.tone_map == Clamp {
      clamp01(exposed)
    } else if !finite(exposed) {
      1.0
    } else {
      exposed / (1.0 + exposed)
    }
  }
  (clamp01(@math.pow(v, 1.0 / options.gamma)) * 255.0 + 0.5).to_int().to_byte()
}

///|
/// RGBA8 diagnostic preview. Assumes input channels already share the desired
/// linear colour space; no ICC/chromaticity conversion or display calibration.
pub fn Image::preview_rgba(
  self : Image,
  rgb~ : Array[String] = ["R", "G", "B"],
  alpha~ : String? = None,
  options~ : PreviewOptions = PreviewOptions::default(),
  limits~ : Limits = Limits::default(),
) -> Bytes raise ExrError {
  limits.validate()
  if rgb.length() != 3 {
    raise Invalid("preview needs three colour channel names", 0)
  }
  if !finite(options.exposure_stops) ||
    options.exposure_stops < -64.0 ||
    options.exposure_stops > 64.0 ||
    !finite(options.gamma) ||
    options.gamma <= 0.0 ||
    options.gamma > 16.0 {
    raise Invalid("preview exposure/gamma range", 0)
  }
  let w = self.data_window
  let count = w.pixels()
  if count > limits.max_pixels || mul(count, 4) > limits.max_decoded_bytes {
    raise Limit("preview bytes")
  }
  let channels = rgb.map(name => self.plane(name))
  let a = alpha.map(name => self.plane(name))
  let scale = @math.pow(2.0, options.exposure_stops)
  let out = sink()
  for row = 0; row < w.height(); row = row + 1 {
    for col = 0; col < w.width(); col = col + 1 {
      let x = w.x0 + col
      let y = w.y0 + row
      let opacity = match a {
        None => 1.0
        Some(p) => clamp01(p.value_at(x, y))
      }
      for p in channels {
        let value = if options.premultiplied_alpha {
          if opacity == 0.0 {
            0.0
          } else {
            p.value_at(x, y) / opacity
          }
        } else {
          p.value_at(x, y)
        }
        out.bytes.push(preview_byte(value, scale, options))
      }
      out.bytes.push((opacity * 255.0 + 0.5).to_int().to_byte())
    }
  }
  out.finish()
}

///|
test "preview maps HDR explicitly without altering sample planes" {
  let w = Window::new(-1, 2, 0, 2)
  let make = fn(name : String, values : Array[Double]) -> Plane raise ExrError {
    Plane::from_values({ name, kind: F32, linear: false, }, w, values)
  }
  let image = Image::new(w, [
    make("R", [2.0, 100.0]),
    make("G", [1.0, -1.0]),
    make("B", [0.5, 0.0]),
    make("A", [0.5, 0.0]),
  ])
  let bytes = image.preview_rgba(alpha=Some("A"), options={
    exposure_stops: 0.0,
    gamma: 1.0,
    tone_map: Clamp,
    premultiplied_alpha: true,
  })
  assert_eq(bytes, b"\xff\xff\xff\x80\x00\x00\x00\x00")
  assert_eq(image.plane("R").value_at(-1, 2), 2.0)
  let mapped = image.preview_rgba(options={
    ..PreviewOptions::default(),
    gamma: 1.0,
  })
  assert_eq(mapped[0], b'\xaa')
  assert_true(
    try {
      ignore(
        image.preview_rgba(options={ ..PreviewOptions::default(), gamma: 0.0, }),
      )
      false
    } catch {
      _ => true
    },
  )
}