///|
/// RGBA color in non-premultiplied form.
/// r/g/b are integers in 0..=255, a is a Double in 0.0..=1.0.
pub struct Color {
  r : Int
  g : Int
  b : Int
  a : Double
} derive(Eq)

///|
pub impl Show for Color with fn output(self, logger) {
  logger.write_string("Color { r: ")
  logger.write_object(self.r)
  logger.write_string(", g: ")
  logger.write_object(self.g)
  logger.write_string(", b: ")
  logger.write_object(self.b)
  logger.write_string(", a: ")
  logger.write_object(self.a)
  logger.write_string(" }")
}

///|
pub fn Color::rgb(r : Int, g : Int, b : Int) -> Color {
  { r, g, b, a: 1.0 }
}

///|
pub fn Color::rgba(r : Int, g : Int, b : Int, a : Double) -> Color {
  { r, g, b, a }
}

///|
pub fn Color::black() -> Color {
  { r: 0, g: 0, b: 0, a: 1.0 }
}

///|
pub fn Color::white() -> Color {
  { r: 255, g: 255, b: 255, a: 1.0 }
}

///|
pub fn Color::transparent() -> Color {
  { r: 0, g: 0, b: 0, a: 0.0 }
}

///|
pub suberror ColorParseError {
  ColorParseError(String)
}

///|
pub impl Show for ColorParseError with fn output(self, logger) {
  let ColorParseError(msg) = self
  logger.write_string("ColorParseError(")
  logger.write_string(msg)
  logger.write_string(")")
}

///|
fn hex_nibble(ch : Char) -> Int raise ColorParseError {
  let code = ch.to_int()
  if code >= 0x30 && code <= 0x39 {
    code - 0x30
  } else if code >= 0x41 && code <= 0x46 {
    code - 0x41 + 10
  } else if code >= 0x61 && code <= 0x66 {
    code - 0x61 + 10
  } else {
    raise ColorParseError("invalid hex digit: " + ch.to_string())
  }
}

///|
fn char_at(src : String, i : Int) -> Char raise ColorParseError {
  match src.get_char(i) {
    Some(c) => c
    None => raise ColorParseError("index out of bounds: " + src)
  }
}

///|
fn parse_hex(src : String) -> Color raise ColorParseError {
  // src begins with '#'. Supported lengths: 4 (#rgb), 5 (#rgba), 7 (#rrggbb), 9 (#rrggbbaa).
  let len = src.length()
  match len {
    4 => {
      let r = hex_nibble(char_at(src, 1)) * 17
      let g = hex_nibble(char_at(src, 2)) * 17
      let b = hex_nibble(char_at(src, 3)) * 17
      { r, g, b, a: 1.0 }
    }
    5 => {
      let r = hex_nibble(char_at(src, 1)) * 17
      let g = hex_nibble(char_at(src, 2)) * 17
      let b = hex_nibble(char_at(src, 3)) * 17
      let a_i = hex_nibble(char_at(src, 4)) * 17
      { r, g, b, a: a_i.to_double() / 255.0 }
    }
    7 => {
      let r = hex_nibble(char_at(src, 1)) * 16 + hex_nibble(char_at(src, 2))
      let g = hex_nibble(char_at(src, 3)) * 16 + hex_nibble(char_at(src, 4))
      let b = hex_nibble(char_at(src, 5)) * 16 + hex_nibble(char_at(src, 6))
      { r, g, b, a: 1.0 }
    }
    9 => {
      let r = hex_nibble(char_at(src, 1)) * 16 + hex_nibble(char_at(src, 2))
      let g = hex_nibble(char_at(src, 3)) * 16 + hex_nibble(char_at(src, 4))
      let b = hex_nibble(char_at(src, 5)) * 16 + hex_nibble(char_at(src, 6))
      let a_i = hex_nibble(char_at(src, 7)) * 16 + hex_nibble(char_at(src, 8))
      { r, g, b, a: a_i.to_double() / 255.0 }
    }
    _ => raise ColorParseError("unsupported hex length: " + src)
  }
}

///|
fn skip_ws(src : String, i : Int) -> Int {
  let mut j = i
  let len = src.length()
  while j < len {
    match src.get_char(j) {
      Some(' ') | Some('\t') | Some('\n') | Some('\r') => j = j + 1
      _ => break
    }
  }
  j
}

///|
fn parse_double_token(
  src : String,
  start : Int,
) -> (Double, Int) raise ColorParseError {
  let mut i = skip_ws(src, start)
  let begin = i
  let len = src.length()
  while i < len {
    match src.get_char(i) {
      Some('0')
      | Some('1')
      | Some('2')
      | Some('3')
      | Some('4')
      | Some('5')
      | Some('6')
      | Some('7')
      | Some('8')
      | Some('9')
      | Some('.')
      | Some('-')
      | Some('+') => i = i + 1
      _ => break
    }
  }
  if begin == i {
    raise ColorParseError("expected number at position " + begin.to_string())
  }
  try @string.parse_double(src[begin:i]) catch {
    _ =>
      raise ColorParseError("invalid number at position " + begin.to_string())
  } noraise {
    v => (v, skip_ws(src, i))
  }
}

///|
fn expect_char(src : String, i : Int, ch : Char) -> Int raise ColorParseError {
  let j = skip_ws(src, i)
  match src.get_char(j) {
    Some(c) if c == ch => skip_ws(src, j + 1)
    _ =>
      raise ColorParseError(
        "expected '" + ch.to_string() + "' at position " + j.to_string(),
      )
  }
}

///|
fn parse_rgb_like(
  src : String,
  with_alpha : Bool,
) -> Color raise ColorParseError {
  let len = src.length()
  let mut i = 0
  let mut found = false
  while i < len {
    match src.get_char(i) {
      Some('(') => {
        found = true
        break
      }
      _ => i = i + 1
    }
  }
  if !found {
    raise ColorParseError("missing '(' in color: " + src)
  }
  i = i + 1
  let (r, i1) = parse_double_token(src, i)
  let i2 = expect_char(src, i1, ',')
  let (g, i3) = parse_double_token(src, i2)
  let i4 = expect_char(src, i3, ',')
  let (b, i5) = parse_double_token(src, i4)
  let (a, i_end) = if with_alpha {
    let i6 = expect_char(src, i5, ',')
    let (av, i7) = parse_double_token(src, i6)
    (av, i7)
  } else {
    (1.0, i5)
  }
  let after_close = expect_char(src, i_end, ')')
  if after_close != src.length() {
    raise ColorParseError("trailing garbage after ')' in color: " + src)
  }
  { r: r.to_int(), g: g.to_int(), b: b.to_int(), a }
}

///|
fn named_color(name : String) -> Color? {
  match name {
    "black" => Some({ r: 0, g: 0, b: 0, a: 1.0 })
    "white" => Some({ r: 255, g: 255, b: 255, a: 1.0 })
    "red" => Some({ r: 255, g: 0, b: 0, a: 1.0 })
    "green" => Some({ r: 0, g: 128, b: 0, a: 1.0 })
    "blue" => Some({ r: 0, g: 0, b: 255, a: 1.0 })
    "yellow" => Some({ r: 255, g: 255, b: 0, a: 1.0 })
    "cyan" => Some({ r: 0, g: 255, b: 255, a: 1.0 })
    "magenta" => Some({ r: 255, g: 0, b: 255, a: 1.0 })
    "gray" => Some({ r: 128, g: 128, b: 128, a: 1.0 })
    "silver" => Some({ r: 192, g: 192, b: 192, a: 1.0 })
    "maroon" => Some({ r: 128, g: 0, b: 0, a: 1.0 })
    "olive" => Some({ r: 128, g: 128, b: 0, a: 1.0 })
    "lime" => Some({ r: 0, g: 255, b: 0, a: 1.0 })
    "teal" => Some({ r: 0, g: 128, b: 128, a: 1.0 })
    "navy" => Some({ r: 0, g: 0, b: 128, a: 1.0 })
    "purple" => Some({ r: 128, g: 0, b: 128, a: 1.0 })
    "transparent" => Some({ r: 0, g: 0, b: 0, a: 0.0 })
    _ => None
  }
}

///|
pub fn Color::parse(css : String) -> Color raise ColorParseError {
  match css.get_char(0) {
    None => raise ColorParseError("empty color string")
    Some('#') => parse_hex(css)
    Some(_) =>
      if css.has_prefix("rgba(") {
        parse_rgb_like(css, true)
      } else if css.has_prefix("rgb(") {
        parse_rgb_like(css, false)
      } else {
        match named_color(css) {
          Some(c) => c
          None => raise ColorParseError("unsupported color: " + css)
        }
      }
  }
}