///|
fn lower_ascii(input : String) -> String {
  let output : Array[Char] = []
  for char in input {
    let code = char.to_int()
    if code >= 'A'.to_int() && code <= 'Z'.to_int() {
      output.push((code + 32).unsafe_to_char())
    } else {
      output.push(char)
    }
  }
  String::from_array(output)
}

///|
fn strip_comment(line : String) -> String {
  match line.split_once("#") {
    Some(parts) => parts.0.to_owned()
    None => line
  }
}

///|
fn strip_carriage_return(line : String) -> String {
  if line.has_suffix("\r") {
    let chars = line.to_array()
    String::from_array(chars[0:chars.length() - 1])
  } else {
    line
  }
}

///|
fn strip_utf8_bom(line : String) -> String {
  let chars = line.to_array()
  if chars.length() > 0 && chars[0] == '\u{FEFF}' {
    String::from_array(chars[1:])
  } else {
    line
  }
}

///|
fn string_slice(input : String, start : Int, end : Int) -> String {
  String::from_array(input.to_array()[start:end])
}

///|
fn string_from(input : String, start : Int) -> String {
  String::from_array(input.to_array()[start:])
}

///|
fn directive_kind(key : String) -> DirectiveKind {
  match lower_ascii(key) {
    "user-agent" => UserAgentDirective
    "allow" => AllowDirective
    "disallow" => DisallowDirective
    "sitemap" => SitemapDirective
    "crawl-delay" => CrawlDelayDirective
    "host" => HostDirective
    "clean-param" => CleanParamDirective
    _ => UnknownDirective
  }
}

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

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

///|
fn is_agent_char(char : Char) -> Bool {
  is_ascii_letter(char) ||
  is_ascii_digit(char) ||
  char == '-' ||
  char == '_' ||
  char == '.'
}

///|
fn valid_agent_token(agent : String) -> Bool {
  if agent == "*" {
    return true
  }
  if agent.length() == 0 {
    return false
  }
  for char in agent {
    if !is_agent_char(char) {
      return false
    }
  }
  true
}

///|
fn is_control_char(char : Char) -> Bool {
  let code = char.to_int()
  (code >= 0 && code < 32 && char != '\t') || code == 127
}

///|
fn contains_control_char(input : String) -> Bool {
  for char in input {
    if is_control_char(char) {
      return true
    }
  }
  false
}

///|
fn parse_positive_decimal(input : String) -> Int {
  if input.length() == 0 {
    return -1
  }
  let mut value = 0
  for char in input {
    if !is_ascii_digit(char) {
      return -1
    }
    value = value * 10 + char.to_int() - '0'.to_int()
    if value > 100000000 {
      return -1
    }
  }
  value
}

///|
fn parse_delay_millis(input : String) -> Int {
  let clean = input.trim().to_owned()
  match clean.split_once(".") {
    None => {
      let seconds = parse_positive_decimal(clean)
      if seconds < 0 {
        -1
      } else {
        seconds * 1000
      }
    }
    Some(parts) => {
      let seconds = parse_positive_decimal(parts.0.to_owned())
      let fraction = parts.1.to_owned()
      if seconds < 0 || fraction.length() == 0 || fraction.length() > 3 {
        return -1
      }
      let fraction_value = parse_positive_decimal(fraction)
      if fraction_value < 0 {
        return -1
      }
      let multiplier = if fraction.length() == 1 {
        100
      } else if fraction.length() == 2 {
        10
      } else {
        1
      }
      seconds * 1000 + fraction_value * multiplier
    }
  }
}

///|
fn hex_value(char : Char) -> Int {
  let code = char.to_int()
  if code >= '0'.to_int() && code <= '9'.to_int() {
    code - '0'.to_int()
  } else if code >= 'A'.to_int() && code <= 'F'.to_int() {
    code - 'A'.to_int() + 10
  } else if code >= 'a'.to_int() && code <= 'f'.to_int() {
    code - 'a'.to_int() + 10
  } else {
    -1
  }
}

///|
fn upper_hex_char(char : Char) -> Char {
  let code = char.to_int()
  if code >= 'a'.to_int() && code <= 'f'.to_int() {
    (code - 32).unsafe_to_char()
  } else {
    char
  }
}

///|
fn is_unreserved_ascii(code : Int) -> Bool {
  (code >= 'A'.to_int() && code <= 'Z'.to_int()) ||
  (code >= 'a'.to_int() && code <= 'z'.to_int()) ||
  (code >= '0'.to_int() && code <= '9'.to_int()) ||
  code == '-'.to_int() ||
  code == '.'.to_int() ||
  code == '_'.to_int() ||
  code == '~'.to_int()
}

///|
/// Decodes percent-encoded unreserved ASCII while preserving reserved octets.
pub fn normalize_percent_encoding(input : String) -> String {
  let chars = input.to_array()
  let output : Array[Char] = []
  let mut index = 0
  while index < chars.length() {
    if chars[index] == '%' && index + 2 < chars.length() {
      let high = hex_value(chars[index + 1])
      let low = hex_value(chars[index + 2])
      if high >= 0 && low >= 0 {
        let code = high * 16 + low
        if is_unreserved_ascii(code) {
          output.push(code.unsafe_to_char())
        } else {
          output.push('%')
          output.push(upper_hex_char(chars[index + 1]))
          output.push(upper_hex_char(chars[index + 2]))
        }
        index = index + 3
        continue
      }
    }
    output.push(chars[index])
    index = index + 1
  }
  String::from_array(output)
}

///|
fn normalize_rule_pattern(pattern : String) -> String {
  normalize_percent_encoding(pattern)
}

///|
/// Normalizes a URL path for RFC 9309 comparison.
pub fn normalize_path(path : String) -> String {
  let clean = if path.length() == 0 {
    "/"
  } else if path.has_prefix("/") {
    path
  } else {
    "/\{path}"
  }
  normalize_percent_encoding(clean)
}

///|
fn rule_specificity(pattern : String) -> Int {
  let chars = pattern.to_array()
  let mut count = 0
  for char in chars {
    if char != '*' && char != '$' {
      count = count + 1
    }
  }
  count
}

///|
fn count_char(input : String, target : Char) -> Int {
  let mut count = 0
  for char in input {
    if char == target {
      count = count + 1
    }
  }
  count
}

///|
fn array_contains_string(items : Array[String], target : String) -> Bool {
  for item in items {
    if item == target {
      return true
    }
  }
  false
}

///|
fn push_unique(items : Array[String], value : String) -> Bool {
  if array_contains_string(items, value) {
    false
  } else {
    items.push(value)
    true
  }
}

///|
fn join_strings(items : Array[String], separator : String) -> String {
  let buffer = StringBuilder::new()
  for index, item in items {
    if index > 0 {
      buffer.write_string(separator)
    }
    buffer.write_string(item)
  }
  buffer.to_string()
}

///|
fn is_absolute_http_url(value : String) -> Bool {
  let lower = lower_ascii(value)
  (lower.has_prefix("http://") && value.length() > 7) ||
  (lower.has_prefix("https://") && value.length() > 8)
}

///|