///|
pub(all) enum TokenProblem {
  EmptyName
  EmptyValue(String)
  UnsupportedColor(String, String)
  UnsupportedLength(String, String)
  UnsupportedShadow(String)
} derive(Debug, Eq)

///|
pub fn TokenProblem::message(self : TokenProblem) -> String {
  match self {
    EmptyName => "token name must not be empty"
    EmptyValue(name) => "token `\{name}` must not have an empty value"
    UnsupportedColor(name, value) =>
      "color token `\{name}` should use a hex color, rgb(), rgba(), hsl(), hwb(), lab(), lch(), oklab(), oklch(), color(), transparent, currentColor, or a CSS variable; got `\{value}`"
    UnsupportedLength(name, value) =>
      "length token `\{name}` should use px, rem, em, %, vw, vh, svw, svh, lvw, lvh, dvw, dvh, ch, ex, cap, lh, rlh, vmin, vmax, or calc(); got `\{value}`"
    UnsupportedShadow(name) =>
      "shadow token `\{name}` should use none, a CSS variable, or a shadow expression with offsets"
  }
}

///|
pub fn Token::validate(self : Token) -> Array[TokenProblem] {
  let problems : Array[TokenProblem] = []
  if self.name == "" {
    problems.push(EmptyName)
  }
  if self.value == "" {
    problems.push(EmptyValue(self.name))
  }
  match self.kind {
    Color =>
      if !is_color_value(self.value) {
        problems.push(UnsupportedColor(self.name, self.value))
      }
    FontSize | Spacing | Radius =>
      if !is_length_value(self.value) {
        problems.push(UnsupportedLength(self.name, self.value))
      }
    Shadow =>
      if !is_shadow_value(self.value) {
        problems.push(UnsupportedShadow(self.name))
      }
  }
  problems
}

///|
pub fn validate_all(tokens : Array[Token]) -> Array[TokenProblem] {
  let problems : Array[TokenProblem] = []
  for token in tokens {
    for problem in token.validate() {
      problems.push(problem)
    }
  }
  problems
}

///|
fn is_color_value(value : String) -> Bool {
  let normalized = value.trim().to_owned()
  is_alias_value(normalized) ||
  normalized == "transparent" ||
  normalized == "currentColor" ||
  normalized == "currentcolor" ||
  is_hex_color(normalized) ||
  is_css_function(normalized, "rgb") ||
  is_css_function(normalized, "rgba") ||
  is_css_function(normalized, "hsl") ||
  is_css_function(normalized, "hsla") ||
  is_css_function(normalized, "hwb") ||
  is_css_function(normalized, "lab") ||
  is_css_function(normalized, "lch") ||
  is_css_function(normalized, "oklab") ||
  is_css_function(normalized, "oklch") ||
  is_css_function(normalized, "color") ||
  is_css_function(normalized, "color-mix") ||
  is_css_function(normalized, "var")
}

///|
fn is_length_value(value : String) -> Bool {
  let normalized = value.trim().to_owned()
  is_alias_value(normalized) ||
  normalized == "0" ||
  is_css_function(normalized, "calc") ||
  is_css_function(normalized, "min") ||
  is_css_function(normalized, "max") ||
  is_css_function(normalized, "clamp") ||
  is_css_function(normalized, "var") ||
  is_length_literal(normalized)
}

///|
fn is_shadow_value(value : String) -> Bool {
  let normalized = value.trim().to_owned()
  if normalized == "none" || is_alias_value(normalized) {
    true
  } else if is_css_function(normalized, "var") {
    true
  } else {
    let mut length_count = 0
    for part in normalized.split(" ").to_array() {
      let candidate = remove_trailing_comma(part.trim().to_owned())
      if candidate != "" && is_length_value(candidate) {
        length_count += 1
      }
    }
    length_count >= 2 &&
    !normalized.has_prefix(",") &&
    !normalized.has_suffix(",")
  }
}

///|
fn is_alias_value(value : String) -> Bool {
  value.length() > 2 && value.has_prefix("{") && value.has_suffix("}")
}

///|
fn is_hex_color(value : String) -> Bool {
  if !value.has_prefix("#") {
    false
  } else {
    let digits = value[1:value.length()].to_owned()
    let length = digits.length()
    (length == 3 || length == 4 || length == 6 || length == 8) &&
    all_hex_digits(digits)
  }
}

///|
fn all_hex_digits(value : String) -> Bool {
  for character in value {
    let valid = (character >= '0' && character <= '9') ||
      (character >= 'a' && character <= 'f') ||
      (character >= 'A' && character <= 'F')
    if !valid {
      return false
    }
  }
  true
}

///|
fn is_css_function(value : String, name : String) -> Bool {
  let prefix = "\{name}("
  if !value.has_prefix(prefix) || !value.has_suffix(")") {
    false
  } else {
    let start = prefix.length()
    let end = value.length() - 1
    let body = value[start:end].to_owned().trim().to_owned()
    body != "" && balanced_parentheses(value)
  }
}

///|
fn remove_trailing_comma(value : String) -> String {
  if value.has_suffix(",") {
    value[0:value.length() - 1].to_owned()
  } else {
    value
  }
}

///|
fn balanced_parentheses(value : String) -> Bool {
  let mut depth = 0
  for character in value {
    if character == '(' {
      depth += 1
    } else if character == ')' {
      depth -= 1
      if depth < 0 {
        return false
      }
    }
  }
  depth == 0
}

///|
fn is_length_literal(value : String) -> Bool {
  let units = [
    "svw", "svh", "lvw", "lvh", "dvw", "dvh", "vmin", "vmax", "rem", "cap", "rlh",
    "px", "em", "vw", "vh", "ch", "ex", "lh", "%",
  ]
  for unit in units {
    if value.has_suffix(unit) {
      let number = value[0:value.length() - unit.length()].to_owned()
      return is_css_number(number)
    }
  }
  false
}

///|
fn is_css_number(value : String) -> Bool {
  let normalized = value.trim().to_owned()
  if normalized == "" {
    false
  } else {
    let mut digits = 0
    let mut dots = 0
    let mut position = 0
    for character in normalized {
      if (character == '+' || character == '-') && position == 0 {
        position += 1
      } else if character == '.' {
        dots += 1
        position += 1
      } else if character >= '0' && character <= '9' {
        digits += 1
        position += 1
      } else {
        return false
      }
    }
    digits > 0 && dots <= 1
  }
}