///|
// The pinned map is entirely ASCII. Python Unicode lower() can produce an
// ASCII name only from ASCII letters and U+212A KELVIN SIGN; all other Unicode
// lowercase results still contain non-ASCII and cannot match that fixed map.
// Keep this projection local: core String.to_lower only changes ASCII letters.
fn line_color_name_lower(value : String) -> String {
  let result = StringBuilder()
  for character in value.to_array() {
    if character >= 'A' && character <= 'Z' {
      result.write_char((character.to_int() + 32).unsafe_to_char())
    } else if character == '\u{212a}' {
      result.write_char('k')
    } else {
      result.write_char(character)
    }
  }
  result.to_string()
}

///|
fn line_color_is_cycle(value : String) -> Bool {
  let characters = value.to_array()
  if characters.length() < 2 || characters[0] != 'C' {
    return false
  }
  for index in 1.. '9' {
      return false
    }
  }
  true
}

///|
fn line_color_hex_digit(character : Char) -> Int32? {
  if character >= '0' && character <= '9' {
    Some(character.to_int() - 48)
  } else if character >= 'a' && character <= 'f' {
    Some(character.to_int() - 87)
  } else if character >= 'A' && character <= 'F' {
    Some(character.to_int() - 55)
  } else {
    None
  }
}

///|
// Native color cache history is deliberately not copied. Equal signed-zero
// cache keys can retain an older alpha sign; this portable helper uses the
// current override sign and keeps the raw Artist setter's separate no-op rule.
fn line_color_override_alpha(
  color : @colors.Rgba,
  alpha : Double?,
) -> @colors.Rgba {
  match alpha {
    None => color
    Some(alpha) =>
      try! @colors.rgba(color.red(), color.green(), color.blue(), alpha~)
  }
}

///|
// This is the source no-colorcycle path. A selected cycle entry must not be
// interpreted as another cycle token. Error text belongs to this primitive,
// whereas validation wraps these ValueErrors with _check_color_like's message.
fn line_color_rgba_no_cycle(
  value : String,
  alpha~ : Double? = None,
) -> @colors.Rgba raise @core.DataError {
  if alpha is Some(value) && !(value >= 0.0 && value <= 1.0) {
    raise @core.DataError::InvalidTicks(
      message="ValueError: 'alpha' must be between 0 and 1, inclusive",
    )
  }
  let lower = line_color_name_lower(value)
  if lower == "none" {
    return try! @colors.rgba(0.0, 0.0, 0.0, alpha=0.0)
  }
  match line_color_named(value) {
    Some(color) => return line_color_override_alpha(color, alpha)
    None => ()
  }
  let characters = value.to_array()
  if characters.length() != 1 {
    match line_color_named(lower) {
      Some(color) => return line_color_override_alpha(color, alpha)
      None => ()
    }
  }
  if characters.length() > 1 && characters[0] == '#' {
    let digits : Array[Int32] = []
    let mut all_hex = true
    for index in 1.. digits.push(digit)
        None => {
          all_hex = false
          break
        }
      }
    }
    if all_hex {
      let (red, green, blue, embedded_alpha) = match digits.length() {
        3 =>
          (
            digits[0].to_double() / 15.0,
            digits[1].to_double() / 15.0,
            digits[2].to_double() / 15.0,
            1.0,
          )
        4 =>
          (
            digits[0].to_double() / 15.0,
            digits[1].to_double() / 15.0,
            digits[2].to_double() / 15.0,
            digits[3].to_double() / 15.0,
          )
        6 =>
          (
            (digits[0] * 16 + digits[1]).to_double() / 255.0,
            (digits[2] * 16 + digits[3]).to_double() / 255.0,
            (digits[4] * 16 + digits[5]).to_double() / 255.0,
            1.0,
          )
        8 =>
          (
            (digits[0] * 16 + digits[1]).to_double() / 255.0,
            (digits[2] * 16 + digits[3]).to_double() / 255.0,
            (digits[4] * 16 + digits[5]).to_double() / 255.0,
            (digits[6] * 16 + digits[7]).to_double() / 255.0,
          )
        _ =>
          raise @core.DataError::InvalidTicks(
            message="ValueError: Invalid hex color specifier: " +
              @ticker.python_string_repr(value),
          )
      }
      return try! @colors.rgba(
        red,
        green,
        blue,
        alpha=alpha.unwrap_or(embedded_alpha),
      )
    }
  }
  let gray = @ticker.parse_float_text(value) catch {
    @core.DataError::InvalidTicks(..) =>
      raise @core.DataError::InvalidTicks(
        message="ValueError: Invalid RGBA argument: " +
          @ticker.python_string_repr(value),
      )
    error => raise error
  }
  if !(gray >= 0.0 && gray <= 1.0) {
    raise @core.DataError::InvalidTicks(
      message="ValueError: Invalid string grayscale value " +
        @ticker.python_string_repr(value) +
        ". Value must be within 0-1 range",
    )
  }
  try! @colors.rgba(gray, gray, gray, alpha=alpha.unwrap_or(1.0))
}

///|
fn line_color_validate(value : String) -> Unit raise @core.DataError {
  // is_color_like accepts the complete ASCII cycle syntax before rc lookup or
  // Python int conversion. The 4300-digit bound is therefore deliberately late.
  if line_color_is_cycle(value) {
    return
  }
  ignore(line_color_rgba_no_cycle(value)) catch {
    @core.DataError::InvalidTicks(..) =>
      raise @core.DataError::InvalidTicks(
        message="ValueError: " +
          @ticker.python_string_repr(value) +
          " is not a valid value for color: supported inputs are " +
          "(r, g, b) and (r, g, b, a) 0-1 float tuples; " +
          "'#rrggbb', '#rrggbbaa', '#rgb', '#rgba' strings; " +
          "named color strings; string reprs of 0-1 floats for grayscale values; " +
          "'C0', 'C1', ... strings for colors of the color cycle; " +
          "and pairs combining one of the above with an alpha value",
      )
    error => raise error
  }
}

///|
fn line_color_rgba(
  value : String,
  alpha~ : Double? = None,
) -> @colors.Rgba raise @core.DataError {
  if !line_color_is_cycle(value) {
    return line_color_rgba_no_cycle(value, alpha~)
  }
  let cycle = @config.color_cycle()
  let digits = value.to_array()
  let count = digits.length() - 1
  if count > 4300 {
    raise @core.DataError::InvalidTicks(
      message="ValueError: Exceeds the limit (4300 digits) for integer string conversion: value has " +
        count.to_string() +
        " digits; use sys.set_int_max_str_digits() to increase the limit",
    )
  }
  // Config guarantees a nonempty cycle. Int64 keeps multiply-add safe even
  // when an Array has the largest representable Int32 length.
  let modulus = cycle.length().to_int64()
  let mut index = 0L
  for offset in 1..