///|
pub enum ContrastGrade {
  Fail
  AaLarge
  Aa
  Aaa
} derive(Debug, Eq)

///|
pub struct ContrastPair {
  foreground : ColorToken
  background : ColorToken
  ratio_x100 : Int
  grade : ContrastGrade
} derive(Debug, Eq)

///|
pub fn ContrastGrade::label(self : ContrastGrade) -> String {
  match self {
    Fail => "fail"
    AaLarge => "AA large text"
    Aa => "AA"
    Aaa => "AAA"
  }
}

///|
pub fn ColorToken::relative_luminance_x10000(self : ColorToken) -> Int {
  match parse_hex_rgb(self.value) {
    Some(rgb) => luminance_x10000(rgb)
    None => 0
  }
}

///|
pub fn ColorToken::contrast_against(
  self : ColorToken,
  background : ColorToken,
) -> ContrastPair {
  let fg = self.relative_luminance_x10000()
  let bg = background.relative_luminance_x10000()
  let lighter = if fg >= bg { fg } else { bg }
  let darker = if fg >= bg { bg } else { fg }
  let ratio = (lighter + 500) * 100 / (darker + 500)
  ContrastPair::{
    foreground: self,
    background,
    ratio_x100: ratio,
    grade: contrast_grade(ratio),
  }
}

///|
pub fn ContrastPair::describe(self : ContrastPair) -> String {
  let whole = self.ratio_x100 / 100
  let fraction = self.ratio_x100 % 100
  "\{self.foreground.name} on \{self.background.name}: \{whole}.\{fraction}:1, \{self.grade.label()}"
}

///|
fn contrast_grade(ratio_x100 : Int) -> ContrastGrade {
  if ratio_x100 >= 700 {
    Aaa
  } else if ratio_x100 >= 450 {
    Aa
  } else if ratio_x100 >= 300 {
    AaLarge
  } else {
    Fail
  }
}

///|
struct Rgb {
  r : Int
  g : Int
  b : Int
} derive(Debug, Eq)

///|
fn parse_hex_rgb(value : String) -> Rgb? {
  if value.length() != 7 || value[0] != '#' {
    None
  } else {
    match
      (
        hex_digit(value[1]),
        hex_digit(value[2]),
        hex_digit(value[3]),
        hex_digit(value[4]),
        hex_digit(value[5]),
        hex_digit(value[6]),
      ) {
      (Some(r1), Some(r2), Some(g1), Some(g2), Some(b1), Some(b2)) =>
        Some(Rgb::{ r: r1 * 16 + r2, g: g1 * 16 + g2, b: b1 * 16 + b2 })
      _ => None
    }
  }
}

///|
fn 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 luminance_x10000(rgb : Rgb) -> Int {
  (
    linear_channel_x10000(rgb.r) * 2126 +
    linear_channel_x10000(rgb.g) * 7152 +
    linear_channel_x10000(rgb.b) * 722
  ) /
  10000
}

///|
fn linear_channel_x10000(channel : Int) -> Int {
  let scaled = channel * 10000 / 255
  if scaled <= 392 {
    scaled * 10000 / 1292
  } else {
    let adjusted = (scaled + 550) * 10000 / 10550
    adjusted * adjusted / 10000
  }
}