// Color helpers for continuous color scales (used by the heatmap).

///|
/// Value of one hex digit; anything else counts as 0.
fn hex_digit(c : Char) -> Int {
  match c {
    '0'..='9' => c.to_int() - 48
    'a'..='f' => c.to_int() - 87
    'A'..='F' => c.to_int() - 55
    _ => 0
  }
}

///|
/// Parse a "#rrggbb" color into `(r, g, b)`. Malformed input degrades to black.
fn parse_hex_color(s : String) -> (Int, Int, Int) {
  if s.length() != 7 {
    return (0, 0, 0)
  }
  let d = fn(i : Int) -> Int {
    match s.get_char(i) {
      Some(c) => hex_digit(c)
      None => 0
    }
  }
  (d(1) * 16 + d(2), d(3) * 16 + d(4), d(5) * 16 + d(6))
}

///|
/// Two lowercase hex digits for a channel value clamped to 0..255.
fn hex2(v : Int) -> String {
  let chars = "0123456789abcdef"
  let v = if v < 0 { 0 } else if v > 255 { 255 } else { v }
  let sb = StringBuilder::new()
  match (chars.get_char(v / 16), chars.get_char(v % 16)) {
    (Some(h), Some(l)) => {
      sb.write_char(h)
      sb.write_char(l)
    }
    _ => sb.write_string("00")
  }
  sb.to_string()
}

///|
/// Linear interpolation between two "#rrggbb" colors; `t` is clamped to
/// `[0, 1]` (`0` gives `a`, `1` gives `b`).
fn lerp_color(a : String, b : String, t : Double) -> String {
  let t = if t < 0.0 { 0.0 } else if t > 1.0 { 1.0 } else { t }
  let (ar, ag, ab) = parse_hex_color(a)
  let (br, bg, bb) = parse_hex_color(b)
  let mix = fn(x : Int, y : Int) -> Int {
    (x.to_double() + (y - x).to_double() * t).round().to_int()
  }
  "#" + hex2(mix(ar, br)) + hex2(mix(ag, bg)) + hex2(mix(ab, bb))
}