///|
fn is_ascii_letter(char : Char) -> Bool {
  (char >= 'a' && char <= 'z') || (char >= 'A' && char <= 'Z')
}

///|
fn is_ascii_digit(char : Char) -> Bool {
  char >= '0' && char <= '9'
}

///|
fn is_identifier(value : String) -> Bool {
  if value.is_empty() || value.length() > 64 {
    return false
  }
  for char in value {
    if !is_ascii_letter(char) &&
      !is_ascii_digit(char) &&
      char != '.' &&
      char != '_' &&
      char != '-' &&
      char != '@' &&
      char != '/' {
      return false
    }
  }
  true
}

///|
fn is_simple_name(value : String) -> Bool {
  if value.is_empty() || value.length() > 64 {
    return false
  }
  for char in value {
    if !is_ascii_letter(char) &&
      !is_ascii_digit(char) &&
      char != '.' &&
      char != '_' &&
      char != '-' {
      return false
    }
  }
  true
}

///|
fn is_principal(value : String) -> Bool {
  if value.length() < 2 || value.length() > 128 || !value.has_prefix("@") {
    return false
  }
  for char in value[1:] {
    if !is_ascii_letter(char) &&
      !is_ascii_digit(char) &&
      char != '.' &&
      char != '_' &&
      char != '-' &&
      char != '/' {
      return false
    }
  }
  true
}

///|
fn contains_string(values : Array[String], expected : String) -> Bool {
  for value in values {
    if value == expected {
      return true
    }
  }
  false
}

///|
fn push_unique_string(values : Array[String], value : String) -> Unit {
  if !contains_string(values, value) {
    values.push(value)
  }
}

///|
fn normalize_names(
  values : Array[String],
  location : String,
) -> Result[Array[String], Diagnostic] {
  let result : Array[String] = []
  for index, value in values {
    if !is_simple_name(value) {
      return Err(
        Diagnostic::new(
          "policy.name.invalid",
          location + "[" + index.to_string() + "]",
          "policy name is invalid",
          "letters, digits, dot, underscore, or hyphen",
          value,
        ),
      )
    }
    push_unique_string(result, value)
  }
  result.sort_by(fn(left, right) { left.lexical_compare(right) })
  Ok(result)
}

///|
fn split_owned_lines(source : String) -> Array[String] {
  source
  .split("\n")
  .map(fn(line) {
    let owned = line.to_owned()
    if owned.has_suffix("\r") {
      owned[:owned.length() - 1].to_owned()
    } else {
      owned
    }
  })
  .collect()
}

///|
fn split_fields(line : String) -> Array[String] {
  line
  .split(" ")
  .filter(fn(field) { !field.is_empty() })
  .map(fn(field) { field.to_owned() })
  .collect()
}

///|
fn split_csv(value : String) -> Array[String] {
  if value == "-" || value.is_empty() {
    []
  } else {
    value.split(",").map(fn(item) { item.to_owned() }).collect()
  }
}

///|
fn parse_decimal(
  value : String,
  location : String,
  maximum : Int,
) -> Result[Int, Diagnostic] {
  if value.is_empty() {
    return Err(
      Diagnostic::new(
        "syntax.integer", location, "integer is empty", "decimal integer", value,
      ),
    )
  }
  let mut result = 0
  for char in value {
    if !is_ascii_digit(char) {
      return Err(
        Diagnostic::new(
          "syntax.integer", location, "value is not a decimal integer", "decimal integer",
          value,
        ),
      )
    }
    result = result * 10 + char.to_int() - '0'.to_int()
    if result > maximum {
      return Err(
        Diagnostic::new(
          "syntax.integer.limit",
          location,
          "integer exceeds the safety limit",
          "at most " + maximum.to_string(),
          value,
        ),
      )
    }
  }
  Ok(result)
}

///|
fn parse_yes_no(value : String, location : String) -> Result[Bool, Diagnostic] {
  match value {
    "yes" => Ok(true)
    "no" => Ok(false)
    _ =>
      Err(
        Diagnostic::new(
          "syntax.boolean", location, "boolean token is invalid", "yes or no", value,
        ),
      )
  }
}

///|
fn is_safe_repo_path(path : String) -> Bool {
  if path.is_empty() ||
    path.length() > 1024 ||
    path.has_prefix("/") ||
    path.has_suffix("/") ||
    path.contains("\\") {
    return false
  }
  for part in path.split("/") {
    if part.is_empty() || part == "." || part == ".." {
      return false
    }
    for char in part {
      if char.to_int() < 32 {
        return false
      }
    }
  }
  true
}