// Port of rich/color.py, rich/color_triplet.py, rich/palette.py and
// rich/terminal_theme.py.

///|
/// One of the color systems supported by terminals.
pub(all) enum ColorSystem {
  Standard
  EightBit
  Truecolor
  Windows
} derive(Eq, Hash, Compare, Debug)

///|
/// Upstream `int(ColorSystem.X)`.
pub fn ColorSystem::to_int(self : ColorSystem) -> Int {
  match self {
    Standard => 1
    EightBit => 2
    Truecolor => 3
    Windows => 4
  }
}

///|
/// Upstream `repr(ColorSystem.X)`, e.g. `"ColorSystem.TRUECOLOR"`.
pub impl Show for ColorSystem with fn output(self, logger) {
  logger.write_string(
    match self {
      Standard => "ColorSystem.STANDARD"
      EightBit => "ColorSystem.EIGHT_BIT"
      Truecolor => "ColorSystem.TRUECOLOR"
      Windows => "ColorSystem.WINDOWS"
    },
  )
}

///|
/// Type of color stored in `Color`.
pub(all) enum ColorType {
  Default
  Standard
  EightBit
  Truecolor
  Windows
} derive(Eq, Hash, Compare, Debug)

///|
pub fn ColorType::to_int(self : ColorType) -> Int {
  match self {
    Default => 0
    Standard => 1
    EightBit => 2
    Truecolor => 3
    Windows => 4
  }
}

///|
/// Upstream `ColorType.X.name`, e.g. `"EIGHT_BIT"`.
pub fn ColorType::name(self : ColorType) -> String {
  match self {
    Default => "DEFAULT"
    Standard => "STANDARD"
    EightBit => "EIGHT_BIT"
    Truecolor => "TRUECOLOR"
    Windows => "WINDOWS"
  }
}

///|
pub impl Show for ColorType with fn output(self, logger) {
  logger.write_string("ColorType." + self.name())
}

///|
/// The red, green, and blue components of a color (0–255 each).
pub(all) struct ColorTriplet {
  red : Int
  green : Int
  blue : Int
} derive(Eq, Hash, Compare, Debug)

///|
pub fn ColorTriplet::new(red : Int, green : Int, blue : Int) -> ColorTriplet {
  { red, green, blue, }
}

///|
fn hex_digit(d : Int) -> Char {
  if d < 10 {
    Int::unsafe_to_char('0'.to_int() + d)
  } else {
    Int::unsafe_to_char('a'.to_int() + d - 10)
  }
}

///|
/// Python's `f"{n:02x}"`.
fn hex2(n : Int) -> String {
  let buf = StringBuilder()
  let mut v = if n < 0 { -n } else { n }
  let chars = []
  while v > 0 {
    chars.push(hex_digit(v % 16))
    v = v / 16
  }
  while chars.length() < 2 {
    chars.push('0')
  }
  if n < 0 {
    buf.write_char('-')
  }
  chars.rev_each(c => buf.write_char(c))
  buf.to_string()
}

///|
/// The color in CSS style, e.g. `"#ff0000"`.
pub fn ColorTriplet::hex(self : ColorTriplet) -> String {
  let { red, green, blue, } = self
  if (red | green | blue) >= 0 && (red | green | blue) < 256 {
    // the usual case: components in 0..=255
    let buf = StringBuilder(size_hint=7)
    buf.write_char('#')
    buf.write_char(hex_digit(red >> 4))
    buf.write_char(hex_digit(red & 15))
    buf.write_char(hex_digit(green >> 4))
    buf.write_char(hex_digit(green & 15))
    buf.write_char(hex_digit(blue >> 4))
    buf.write_char(hex_digit(blue & 15))
    return buf.to_string()
  }
  "#" + hex2(red) + hex2(green) + hex2(blue)
}

///|
/// The color in RGB format, e.g. `"rgb(100,23,255)"`.
pub fn ColorTriplet::rgb(self : ColorTriplet) -> String {
  "rgb(\{self.red},\{self.green},\{self.blue})"
}

///|
/// Components as floats between 0 and 1.
pub fn ColorTriplet::normalized(
  self : ColorTriplet,
) -> (Double, Double, Double) {
  (
    self.red.to_double() / 255.0,
    self.green.to_double() / 255.0,
    self.blue.to_double() / 255.0,
  )
}

///|
/// Upstream `repr(ColorTriplet)`.
pub impl Show for ColorTriplet with fn output(self, logger) {
  logger.write_string(
    "ColorTriplet(red=\{self.red}, green=\{self.green}, blue=\{self.blue})",
  )
}

///|
/// A palette of available colors.
pub struct Palette {
  priv colors : Array[(Int, Int, Int)]
  priv cache : Map[(Int, Int, Int), Int]
}

///|
pub fn Palette::new(colors : Array[(Int, Int, Int)]) -> Palette {
  { colors, cache: Map([]), }
}

///|
pub fn Palette::length(self : Palette) -> Int {
  self.colors.length()
}

///|
pub fn Palette::at(self : Palette, number : Int) -> ColorTriplet {
  let (r, g, b) = self.colors[number]
  { red: r, green: g, blue: b, }
}

///|
pub fn Palette::colors(self : Palette) -> Array[(Int, Int, Int)] {
  self.colors.copy()
}

///|
/// Find a color from a palette that most closely matches a given color.
pub fn Palette::match_color(self : Palette, color : ColorTriplet) -> Int {
  let key = (color.red, color.green, color.blue)
  if self.cache.get(key) is Some(index) {
    return index
  }
  let red1 = color.red
  let green1 = color.green
  let blue1 = color.blue
  let mut min_index = 0
  let mut min_distance = 0
  for index, c in self.colors {
    let (red2, green2, blue2) = c
    let red_mean = floor_div(red1 + red2, 2)
    let red = red1 - red2
    let green = green1 - green2
    let blue = blue1 - blue2
    // sqrt is monotonic: comparing the squared distances picks the same index
    let distance = (((512 + red_mean) * red * red) >> 8) +
      4 * green * green +
      (((767 - red_mean) * blue * blue) >> 8)
    if index == 0 || distance < min_distance {
      min_distance = distance
      min_index = index
    }
  }
  self.cache[key] = min_index
  min_index
}

///|
fn floor_div(a : Int, b : Int) -> Int {
  let q = a / b
  if a % b != 0 && (a < 0) != (b < 0) {
    q - 1
  } else {
    q
  }
}

///|
/// A color theme used when exporting console content.
pub struct TerminalTheme {
  background_color : ColorTriplet
  foreground_color : ColorTriplet
  ansi_colors : Palette
}

///|
/// `normal` holds 8 normal intensity colors; `bright` the 8 bright ones (or
/// `normal` again when omitted).
pub fn TerminalTheme::new(
  background : (Int, Int, Int),
  foreground : (Int, Int, Int),
  normal : Array[(Int, Int, Int)],
  bright? : Array[(Int, Int, Int)],
) -> TerminalTheme {
  let colors = normal.copy()
  match bright {
    Some(b) if !b.is_empty() => colors.append(b)
    _ => colors.append(normal)
  }
  {
    background_color: {
      red: background.0,
      green: background.1,
      blue: background.2,
    },
    foreground_color: {
      red: foreground.0,
      green: foreground.1,
      blue: foreground.2,
    },
    ansi_colors: Palette::new(colors),
  }
}

///|
/// Error raised when a color definition can not be parsed.
pub suberror ColorParseError {
  ColorParseError(String)
} derive(Debug)

///|
pub impl Show for ColorParseError with fn output(self, logger) {
  match self {
    ColorParseError(message) => logger.write_string(message)
  }
}

///|
/// Terminal color definition.
pub(all) struct Color {
  /// The name of the color (typically the input to `Color::parse`).
  name : String
  /// The type of the color.
  type_ : ColorType
  /// The color number, if a standard color.
  number : Int?
  /// A triplet of color components, if an RGB color.
  triplet : ColorTriplet?
} derive(Eq, Hash, Debug)

///|
pub fn Color::new(
  name : String,
  type_ : ColorType,
  number? : Int,
  triplet? : ColorTriplet,
) -> Color {
  { name, type_, number, triplet, }
}

///|
/// Upstream `repr(Color)`, e.g.
/// `Color('red', ColorType.STANDARD, number=1)`.
pub impl Show for Color with fn output(self, logger) {
  logger.write_string("Color(")
  logger.write_string(py_repr_str(self.name))
  logger.write_string(", ")
  logger.write_string("ColorType." + self.type_.name())
  if self.number is Some(n) {
    logger.write_string(", number=\{n}")
  }
  if self.triplet is Some(t) {
    logger.write_string(", triplet=\{t}")
  }
  logger.write_string(")")
}

///|
/// Python `repr()` of a string (single quotes unless the string contains a
/// single quote and no double quote).
fn py_repr_str(s : String) -> String {
  let quote = if s.contains("'") && !s.contains("\"") { '"' } else { '\'' }
  let buf = StringBuilder()
  buf.write_char(quote)
  for c in s {
    match c {
      '\\' => buf.write_string("\\\\")
      '\n' => buf.write_string("\\n")
      '\r' => buf.write_string("\\r")
      '\t' => buf.write_string("\\t")
      _ =>
        if c == quote {
          buf.write_char('\\')
          buf.write_char(c)
        } else {
          buf.write_char(c)
        }
    }
  }
  buf.write_char(quote)
  buf.to_string()
}

///|
/// The native color system for this color.
pub fn Color::system(self : Color) -> ColorSystem {
  match self.type_ {
    Default | Standard => Standard
    EightBit => EightBit
    Truecolor => Truecolor
    Windows => Windows
  }
}

///|
/// Check if the color is ultimately defined by the system.
pub fn Color::is_system_defined(self : Color) -> Bool {
  !(self.system() is (EightBit | Truecolor))
}

///|
/// Check if the color is a default color.
pub fn Color::is_default(self : Color) -> Bool {
  self.type_ is Default
}

///|
/// Get an equivalent color triplet for this color.
pub fn Color::get_truecolor(
  self : Color,
  theme? : TerminalTheme,
  foreground? : Bool = true,
) -> ColorTriplet {
  let theme = theme.unwrap_or(default_terminal_theme)
  match self.type_ {
    Truecolor => self.triplet.unwrap()
    EightBit => eight_bit_palette.at(self.number.unwrap())
    Standard => theme.ansi_colors.at(self.number.unwrap())
    Windows => windows_palette.at(self.number.unwrap())
    Default =>
      if foreground {
        theme.foreground_color
      } else {
        theme.background_color
      }
  }
}

///|
/// Create a Color from its 8-bit ANSI number.
pub fn Color::from_ansi(number : Int) -> Color {
  {
    name: "color(\{number})",
    type_: if number < 16 {
      Standard
    } else {
      EightBit
    },
    number: Some(number),
    triplet: None,
  }
}

///|
/// Create a truecolor RGB color from a triplet of values.
pub fn Color::from_triplet(triplet : ColorTriplet) -> Color {
  {
    name: triplet.hex(),
    type_: Truecolor,
    number: None,
    triplet: Some(triplet),
  }
}

///|
/// Create a truecolor from three color components in the range 0–255.
/// Components are truncated towards zero like Python's `int()`.
pub fn Color::from_rgb(red : Double, green : Double, blue : Double) -> Color {
  Color::from_triplet({
    red: red.to_int(),
    green: green.to_int(),
    blue: blue.to_int(),
  })
}

///|
/// A Color instance representing the default color.
pub fn Color::default() -> Color {
  { name: "default", type_: Default, number: None, triplet: None, }
}

///|
let parse_cache : @memo.Memo[String, Color] = @memo.Memo::new(1024)

///|
/// Python `str.isspace()` for a single character (also what `\s` matches).
fn is_py_space(c : Char) -> Bool {
  match c {
    ' ' | '\t' | '\n' | '\r' | '\u{0b}' | '\u{0c}' => true
    '\u{1c}' | '\u{1d}' | '\u{1e}' | '\u{1f}' | '\u{85}' | '\u{a0}' => true
    '\u{1680}'
    | '\u{2028}'
    | '\u{2029}'
    | '\u{202f}'
    | '\u{205f}'
    | '\u{3000}' => true
    _ => c.to_int() >= 0x2000 && c.to_int() <= 0x200a
  }
}

///|
/// Python `str.strip()`.
fn py_strip(s : String) -> String {
  let chars = s.to_array()
  let mut start = 0
  let mut end = chars.length()
  while start < end && is_py_space(chars[start]) {
    start += 1
  }
  while end > start && is_py_space(chars[end - 1]) {
    end -= 1
  }
  String::from_array(chars[start:end])
}

///|
/// The zero of each run of ten Unicode decimal digits (category Nd).
let decimal_zeros : FixedArray[Int] = [
  0x30, 0x660, 0x6F0, 0x7C0, 0x966, 0x9E6, 0xA66, 0xAE6, 0xB66, 0xBE6, 0xC66, 0xCE6,
  0xD66, 0xDE6, 0xE50, 0xED0, 0xF20, 0x1040, 0x1090, 0x17E0, 0x1810, 0x1946, 0x19D0,
  0x1A80, 0x1A90, 0x1B50, 0x1BB0, 0x1C40, 0x1C50, 0xA620, 0xA8D0, 0xA900, 0xA9D0,
  0xA9F0, 0xAA50, 0xABF0, 0xFF10, 0x104A0, 0x10D30, 0x10D40, 0x11066, 0x110F0, 0x11136,
  0x111D0, 0x112F0, 0x11450, 0x114D0, 0x11650, 0x116C0, 0x116D0, 0x116DA, 0x11730,
  0x118E0, 0x11950, 0x11BF0, 0x11C50, 0x11D50, 0x11DA0, 0x11F50, 0x16130, 0x16A60,
  0x16AC0, 0x16B50, 0x16D70, 0x1CCF0, 0x1D7CE, 0x1D7D8, 0x1D7E2, 0x1D7EC, 0x1D7F6,
  0x1E140, 0x1E2F0, 0x1E4F0, 0x1E5F1, 0x1E950, 0x1FBF0,
]

///|
/// The value of a Unicode decimal digit (what Python's `\d` matches and
/// `int()` accepts), or `None`.
fn decimal_value(c : Char) -> Int? {
  let n = c.to_int()
  for zero in decimal_zeros {
    if n >= zero && n < zero + 10 {
      return Some(n - zero)
    }
  }
  None
}

///|
/// Python `int(s, base)` for bases 10 and 16: surrounding whitespace, an
/// optional sign and single underscores between digits are allowed.
fn py_int_base(s : String, base : Int) -> Int? {
  let chars = py_strip(s).to_array()
  let mut i = 0
  let mut negative = false
  if i < chars.length() && chars[i] is ('+' | '-') {
    negative = chars[i] == '-'
    i += 1
  }
  if i >= chars.length() {
    return None
  }
  let mut value = 0
  let mut last_underscore = true
  while i < chars.length() {
    let c = chars[i]
    let digit = match c {
      '_' if !last_underscore => {
        last_underscore = true
        i += 1
        continue
      }
      'a'..='f' if base == 16 => c.to_int() - 'a'.to_int() + 10
      'A'..='F' if base == 16 => c.to_int() - 'A'.to_int() + 10
      _ =>
        match decimal_value(c) {
          Some(d) => d
          None => return None
        }
    }
    value = value * base + digit
    last_underscore = false
    i += 1
  }
  if last_underscore {
    return None
  }
  Some(if negative { -value } else { value })
}

///|
fn all_chars(s : StringView, pred : (Char) -> Bool) -> Bool {
  for c in s {
    if !pred(c) {
      return false
    }
  }
  true
}

///|
/// Python `int(s)` for decimal strings (surrounding whitespace allowed).
fn py_int(s : String) -> Int? {
  py_int_base(s, 10)
}

///|
fn hex_value(s : StringView) -> Int {
  let mut v = 0
  for c in s {
    let d = match c {
      '0'..='9' => c.to_int() - '0'.to_int()
      'a'..='f' => c.to_int() - 'a'.to_int() + 10
      'A'..='F' => c.to_int() - 'A'.to_int() + 10
      _ => 0
    }
    v = v * 16 + d
  }
  v
}

///|
/// Parse a color definition: a color name (`"red"`, `"bright_blue"`,
/// `"grey50"`), `"default"`, `"#rrggbb"`, `"color(N)"` or `"rgb(r,g,b)"`.
pub fn Color::parse(color : String) -> Color raise ColorParseError {
  if parse_cache.get(color) is Some(c) {
    return c
  }
  let result = parse_color(color)
  parse_cache.set(color, result)
  result
}

///|
fn parse_color(original_color : String) -> Color raise ColorParseError {
  let color = py_strip(@pystr.lower(original_color))
  if color == "default" {
    return { name: color, type_: Default, number: None, triplet: None, }
  }
  if ansi_color_names.get(color) is Some(color_number) {
    return {
      name: color,
      type_: if color_number < 16 {
        Standard
      } else {
        EightBit
      },
      number: Some(color_number),
      triplet: None,
    }
  }
  // RE_COLOR = ^\#([0-9a-f]{6})$|color\(([0-9]{1,3})\)$|rgb\(([\d\s,]+)\)$
  // (`$` also matches before a final newline)
  let body = match color.strip_suffix("\n") {
    Some(b) => b.to_owned()
    None => color
  }
  if body.strip_prefix("#") is Some(h) &&
    h.length() == 6 &&
    all_chars(h, c => c is ('0'..='9' | 'a'..='f')) {
    let triplet : ColorTriplet = {
      red: hex_value(h.view(start_offset=0, end_offset=2)),
      green: hex_value(h.view(start_offset=2, end_offset=4)),
      blue: hex_value(h.view(start_offset=4, end_offset=6)),
    }
    return {
      name: color,
      type_: Truecolor,
      number: None,
      triplet: Some(triplet),
    }
  }
  if body.strip_prefix("color(") is Some(rest) &&
    rest.strip_suffix(")") is Some(inner) {
    if inner.length() >= 1 &&
      inner.length() <= 3 &&
      all_chars(inner, c => c is ('0'..='9')) {
      let number = hex_free_int(inner)
      if number > 255 {
        raise ColorParseError(
          "color number must be <= 255 in \{py_repr_str(color)}",
        )
      }
      return {
        name: color,
        type_: if number < 16 {
          Standard
        } else {
          EightBit
        },
        number: Some(number),
        triplet: None,
      }
    }
  }
  if body.strip_prefix("rgb(") is Some(rest) &&
    rest.strip_suffix(")") is Some(inner) {
    if inner.length() >= 1 &&
      all_chars(inner, c => {
        c is ('0'..='9' | ',') || is_unicode_digit(c) || is_py_space(c)
      }) {
      let components = inner
        .to_owned()
        .split(",")
        .map(v => v.to_owned())
        .collect()
      if components.length() != 3 {
        raise ColorParseError(
          "expected three components in \{py_repr_str(original_color)}",
        )
      }
      let values = []
      for component in components {
        match py_int(component) {
          Some(v) => values.push(v)
          None =>
            raise ColorParseError(
              "invalid literal for int() with base 10: \{py_repr_str(component)}",
            )
        }
      }
      let triplet : ColorTriplet = {
        red: values[0],
        green: values[1],
        blue: values[2],
      }
      if values.iter().any(v => v > 255) {
        raise ColorParseError(
          "color components must be <= 255 in \{py_repr_str(original_color)}",
        )
      }
      return {
        name: color,
        type_: Truecolor,
        number: None,
        triplet: Some(triplet),
      }
    }
  }
  raise ColorParseError("\{py_repr_str(original_color)} is not a valid color")
}

///|
fn is_unicode_digit(c : Char) -> Bool {
  decimal_value(c) is Some(_)
}

///|
fn hex_free_int(s : StringView) -> Int {
  let mut v = 0
  for c in s {
    v = v * 10 + (c.to_int() - '0'.to_int())
  }
  v
}

///|
/// Get the ANSI escape codes for this color.
pub fn Color::get_ansi_codes(
  self : Color,
  foreground? : Bool = true,
) -> Array[String] {
  match self.type_ {
    Default => [if foreground { "39" } else { "49" }]
    Windows | Standard => {
      let number = self.number.unwrap()
      let (fore, back) = if number < 8 { (30, 40) } else { (82, 92) }
      [(if foreground { fore + number } else { back + number }).to_string()]
    }
    EightBit =>
      [
        if foreground {
          "38"
        } else {
          "48"
        },
        "5",
        self.number.unwrap().to_string(),
      ]
    Truecolor => {
      let t = self.triplet.unwrap()
      [
        if foreground {
          "38"
        } else {
          "48"
        },
        "2",
        t.red.to_string(),
        t.green.to_string(),
        t.blue.to_string(),
      ]
    }
  }
}

///|
/// Python's `round()` (round half to even) for a non-negative double.
pub fn py_round(x : Double) -> Int {
  let f = x.floor()
  let diff = x - f
  if diff > 0.5 {
    f.to_int() + 1
  } else if diff < 0.5 {
    f.to_int()
  } else if f.to_int() % 2 == 0 {
    f.to_int()
  } else {
    f.to_int() + 1
  }
}

///|
/// Python's `colorsys.rgb_to_hls`.
pub fn rgb_to_hls(
  r : Double,
  g : Double,
  b : Double,
) -> (Double, Double, Double) {
  let maxc = @cmp.maximum(r, @cmp.maximum(g, b))
  let minc = @cmp.minimum(r, @cmp.minimum(g, b))
  let sumc = maxc + minc
  let rangec = maxc - minc
  let l = sumc / 2.0
  if minc == maxc {
    return (0.0, l, 0.0)
  }
  let s = if l <= 0.5 { rangec / sumc } else { rangec / (2.0 - maxc - minc) }
  let rc = (maxc - r) / rangec
  let gc = (maxc - g) / rangec
  let bc = (maxc - b) / rangec
  let h = if r == maxc {
    bc - gc
  } else if g == maxc {
    2.0 + rc - bc
  } else {
    4.0 + gc - rc
  }
  let h = h / 6.0
  // Python's % on floats takes the sign of the divisor
  let h = h - (h / 1.0).floor()
  (h, l, s)
}

///|
/// Downgrade a color system to a system with fewer colors.
pub fn Color::downgrade(self : Color, system : ColorSystem) -> Color {
  if self.type_ is Default || self.type_.to_int() == system.to_int() {
    return self
  }
  match system {
    EightBit if self.system() is Truecolor => {
      let t = self.triplet.unwrap()
      let (r, g, b) = t.normalized()
      let (_, l, s) = rgb_to_hls(r, g, b)
      if s < 0.15 {
        let gray = py_round(l * 25.0)
        let color_number = if gray == 0 {
          16
        } else if gray == 25 {
          231
        } else {
          231 + gray
        }
        return {
          name: self.name,
          type_: EightBit,
          number: Some(color_number),
          triplet: None,
        }
      }
      fn six(v : Int) -> Double {
        if v < 95 {
          v.to_double() / 95.0
        } else {
          1.0 + (v - 95).to_double() / 40.0
        }
      }

      let color_number = 16 +
        36 * py_round(six(t.red)) +
        6 * py_round(six(t.green)) +
        py_round(six(t.blue))
      {
        name: self.name,
        type_: EightBit,
        number: Some(color_number),
        triplet: None,
      }
    }
    Standard => {
      let triplet = if self.system() is Truecolor {
        self.triplet.unwrap()
      } else {
        eight_bit_palette.at(self.number.unwrap())
      }
      let color_number = standard_palette.match_color(triplet)
      {
        name: self.name,
        type_: Standard,
        number: Some(color_number),
        triplet: None,
      }
    }
    Windows => {
      let triplet = if self.system() is Truecolor {
        self.triplet.unwrap()
      } else {
        let number = self.number.unwrap()
        if number < 16 {
          return {
            name: self.name,
            type_: Windows,
            number: Some(number),
            triplet: None,
          }
        }
        eight_bit_palette.at(number)
      }
      let color_number = windows_palette.match_color(triplet)
      {
        name: self.name,
        type_: Windows,
        number: Some(color_number),
        triplet: None,
      }
    }
    _ => self
  }
}

///|
/// Parse six hex characters in to an RGB triplet.
///
/// Aborts on invalid hex digits (upstream raises `ValueError`).
pub fn parse_rgb_hex(hex_color : String) -> ColorTriplet {
  guard hex_color.length() == 6 else { abort("must be 6 characters") }
  fn component(start : Int) -> Int {
    let s = hex_color.view(start_offset=start, end_offset=start + 2).to_owned()
    match py_int_base(s, 16) {
      Some(v) => v
      None => abort("invalid literal for int() with base 16: \{py_repr_str(s)}")
    }
  }

  { red: component(0), green: component(2), blue: component(4), }
}

///|
/// Blend one RGB color in to another.
pub fn blend_rgb(
  color1 : ColorTriplet,
  color2 : ColorTriplet,
  cross_fade? : Double = 0.5,
) -> ColorTriplet {
  fn mix(a : Int, b : Int) -> Int {
    (a.to_double() + (b - a).to_double() * cross_fade).to_int()
  }

  {
    red: mix(color1.red, color2.red),
    green: mix(color1.green, color2.green),
    blue: mix(color1.blue, color2.blue),
  }
}

///|
/// The value of a Unicode decimal digit (category Nd), as accepted by
/// Python's `int()`, e.g. `'٣'` → `Some(3)`.
pub fn unicode_decimal_value(c : Char) -> Int? {
  decimal_value(c)
}