///|
/// An integer RGB value used for deterministic token analysis.
pub struct RgbColor {
  red : Int
  green : Int
  blue : Int
} derive(Debug, Eq)

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

///|
pub fn RgbColor::black() -> RgbColor {
  RgbColor::new(0, 0, 0)
}

///|
pub fn RgbColor::white() -> RgbColor {
  RgbColor::new(255, 255, 255)
}

///|
pub fn RgbColor::from_hex(value : String) -> RgbColor? {
  let text = value.trim().to_owned()
  let body = if text.has_prefix("#") { text[1:].to_owned() } else { text }
  if body.length() == 3 {
    let r = rgb_hex_digit(body[0])
    let g = rgb_hex_digit(body[1])
    let b = rgb_hex_digit(body[2])
    match (r, g, b) {
      (Some(red), Some(green), Some(blue)) =>
        Some(RgbColor::new(red * 17, green * 17, blue * 17))
      _ => None
    }
  } else if body.length() == 6 {
    let r1 = rgb_hex_digit(body[0])
    let r2 = rgb_hex_digit(body[1])
    let g1 = rgb_hex_digit(body[2])
    let g2 = rgb_hex_digit(body[3])
    let b1 = rgb_hex_digit(body[4])
    let b2 = rgb_hex_digit(body[5])
    match (r1, r2, g1, g2, b1, b2) {
      (Some(a), Some(b), Some(c), Some(d), Some(e), Some(f)) =>
        Some(RgbColor::new(a * 16 + b, c * 16 + d, e * 16 + f))
      _ => None
    }
  } else {
    None
  }
}

///|
pub fn RgbColor::to_hex(self : RgbColor) -> String {
  "#" + hex_pair(self.red) + hex_pair(self.green) + hex_pair(self.blue)
}

///|
pub fn RgbColor::to_css_rgb(self : RgbColor) -> String {
  "rgb(" +
  self.red.to_string() +
  ", " +
  self.green.to_string() +
  ", " +
  self.blue.to_string() +
  ")"
}

///|
pub fn RgbColor::channel(self : RgbColor, name : String) -> Int? {
  match normalize_identifier(name) {
    "red" => Some(self.red)
    "green" => Some(self.green)
    "blue" => Some(self.blue)
    _ => None
  }
}

///|
pub fn RgbColor::is_light(self : RgbColor) -> Bool {
  self.luminance_x10000() >= 2500
}

///|
pub fn RgbColor::is_dark(self : RgbColor) -> Bool {
  !self.is_light()
}

///|
pub fn RgbColor::is_grayscale(self : RgbColor) -> Bool {
  self.red == self.green && self.green == self.blue
}

///|
pub fn RgbColor::inverted(self : RgbColor) -> RgbColor {
  RgbColor::new(255 - self.red, 255 - self.green, 255 - self.blue)
}

///|
pub fn RgbColor::complementary(self : RgbColor) -> RgbColor {
  self.inverted()
}

///|
pub fn RgbColor::lighten(self : RgbColor, percent : Int) -> RgbColor {
  self.blend(RgbColor::white(), percent)
}

///|
pub fn RgbColor::darken(self : RgbColor, percent : Int) -> RgbColor {
  self.blend(RgbColor::black(), percent)
}

///|
pub fn RgbColor::blend(
  self : RgbColor,
  other : RgbColor,
  percent : Int,
) -> RgbColor {
  let ratio = clamp_percent(percent)
  let inverse = 100 - ratio
  RgbColor::new(
    (self.red * inverse + other.red * ratio + 50) / 100,
    (self.green * inverse + other.green * ratio + 50) / 100,
    (self.blue * inverse + other.blue * ratio + 50) / 100,
  )
}

///|
pub fn RgbColor::average(self : RgbColor, other : RgbColor) -> RgbColor {
  self.blend(other, 50)
}

///|
pub fn RgbColor::distance_to(self : RgbColor, other : RgbColor) -> Int {
  let red = self.red - other.red
  let green = self.green - other.green
  let blue = self.blue - other.blue
  integer_sqrt(red * red + green * green + blue * blue)
}

///|
pub fn RgbColor::luminance_x10000(self : RgbColor) -> Int {
  (
    2126 * linear_channel(self.red) +
    7152 * linear_channel(self.green) +
    722 * linear_channel(self.blue)
  ) /
  10000
}

///|
pub fn RgbColor::contrast_ratio_x100(self : RgbColor, other : RgbColor) -> Int {
  let first = self.luminance_x10000() + 1000
  let second = other.luminance_x10000() + 1000
  if first > second {
    first * 100 / second
  } else {
    second * 100 / first
  }
}

///|
pub fn RgbColor::with_red(self : RgbColor, red : Int) -> RgbColor {
  RgbColor::new(red, self.green, self.blue)
}

///|
pub fn RgbColor::with_green(self : RgbColor, green : Int) -> RgbColor {
  RgbColor::new(self.red, green, self.blue)
}

///|
pub fn RgbColor::with_blue(self : RgbColor, blue : Int) -> RgbColor {
  RgbColor::new(self.red, self.green, blue)
}

///|
pub fn RgbColor::same_as(self : RgbColor, other : RgbColor) -> Bool {
  self == other
}

///|
pub fn RgbColor::describe(self : RgbColor) -> String {
  self.to_hex() + " (" + self.to_css_rgb() + ")"
}

///|
pub fn ColorToken::rgb(self : ColorToken) -> RgbColor? {
  RgbColor::from_hex(self.value)
}

///|
pub fn ColorToken::lighten(self : ColorToken, percent : Int) -> ColorToken {
  match self.rgb() {
    Some(color) => { ..self, value: color.lighten(percent).to_hex() }
    None => self
  }
}

///|
pub fn ColorToken::darken(self : ColorToken, percent : Int) -> ColorToken {
  match self.rgb() {
    Some(color) => { ..self, value: color.darken(percent).to_hex() }
    None => self
  }
}

///|
pub fn ColorToken::blend_with(
  self : ColorToken,
  other : ColorToken,
  percent : Int,
) -> ColorToken {
  match (self.rgb(), other.rgb()) {
    (Some(first), Some(second)) =>
      { ..self, value: first.blend(second, percent).to_hex() }
    _ => self
  }
}

///|
pub fn Palette::color_named(self : Palette, name : String) -> ColorToken? {
  for color in self.colors {
    if color.name == name {
      return Some(color)
    }
  }
  None
}

///|
pub fn Palette::nearest_color(self : Palette, value : RgbColor) -> ColorToken? {
  let mut best : ColorToken? = None
  let mut best_distance = 1000000
  for color in self.colors {
    match color.rgb() {
      Some(candidate) => {
        let distance = candidate.distance_to(value)
        if distance < best_distance {
          best_distance = distance
          best = Some(color)
        }
      }
      None => continue
    }
  }
  best
}

///|
pub fn Palette::light_variant(self : Palette, percent : Int) -> Palette {
  {
    colors: self.colors.map(fn(color) { color.lighten(percent) }),
    gradients: self.gradients,
  }
}

///|
pub fn Palette::dark_variant(self : Palette, percent : Int) -> Palette {
  {
    colors: self.colors.map(fn(color) { color.darken(percent) }),
    gradients: self.gradients,
  }
}

///|
pub fn Palette::color_names(self : Palette) -> Array[String] {
  self.colors.map(fn(color) { color.name })
}

///|
pub fn Palette::hex_values(self : Palette) -> Array[String] {
  self.colors.map(fn(color) { color.value })
}

///|
pub fn Palette::light_colors(self : Palette) -> Array[ColorToken] {
  self.colors.filter(fn(color) {
    match color.rgb() {
      Some(value) => value.is_light()
      None => false
    }
  })
}

///|
pub fn Palette::dark_colors(self : Palette) -> Array[ColorToken] {
  self.colors.filter(fn(color) {
    match color.rgb() {
      Some(value) => value.is_dark()
      None => false
    }
  })
}

///|
fn clamp_channel(value : Int) -> Int {
  if value < 0 {
    0
  } else if value > 255 {
    255
  } else {
    value
  }
}

///|
fn clamp_percent(value : Int) -> Int {
  if value < 0 {
    0
  } else if value > 100 {
    100
  } else {
    value
  }
}

///|
fn rgb_hex_digit(ch : UInt16) -> Int? {
  if ch >= '0' && ch <= '9' {
    Some(ch.to_int() - ('0' : UInt16).to_int())
  } else if ch >= 'A' && ch <= 'F' {
    Some(ch.to_int() - ('A' : UInt16).to_int() + 10)
  } else if ch >= 'a' && ch <= 'f' {
    Some(ch.to_int() - ('a' : UInt16).to_int() + 10)
  } else {
    None
  }
}

///|
fn hex_pair(value : Int) -> String {
  let safe = clamp_channel(value)
  hex_char(safe / 16) + hex_char(safe % 16)
}

///|
fn hex_char(value : Int) -> String {
  match value {
    0 => "0"
    1 => "1"
    2 => "2"
    3 => "3"
    4 => "4"
    5 => "5"
    6 => "6"
    7 => "7"
    8 => "8"
    9 => "9"
    10 => "A"
    11 => "B"
    12 => "C"
    13 => "D"
    14 => "E"
    _ => "F"
  }
}

///|
fn linear_channel(value : Int) -> Int {
  let square = value * value
  square * 10000 / (255 * 255)
}

///|
fn integer_sqrt(value : Int) -> Int {
  if value <= 0 {
    0
  } else {
    let mut guess = value
    for _ in 0..<16 {
      let next = (guess + value / guess) / 2
      if next >= guess {
        break
      }
      guess = next
    }
    guess
  }
}