// RFC 9309 path pattern normalization and matching.

///|
fn hex_value(ch : Char) -> Int {
  let code = ch.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 hex_digit(value : Int) -> Char {
  if value < 10 {
    ('0'.to_int() + value).unsafe_to_char()
  } else {
    ('A'.to_int() + value - 10).unsafe_to_char()
  }
}

///|
fn write_percent_encoded(out : StringBuilder, code : Int) -> Unit {
  out.write_char('%')
  out.write_char(hex_digit(code / 16))
  out.write_char(hex_digit(code % 16))
}

///|
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()
}

///|
fn normalize_percent_encoding(input : String) -> String {
  let bytes = @utf8.encode(input)
  let out = StringBuilder::new()
  let mut i = 0
  while i < bytes.length() {
    let code = bytes[i].to_int()
    if code == '%'.to_int() && i + 2 < bytes.length() {
      let hi = hex_value(bytes[i + 1].to_int().unsafe_to_char())
      let lo = hex_value(bytes[i + 2].to_int().unsafe_to_char())
      if hi >= 0 && lo >= 0 {
        let decoded = hi * 16 + lo
        if is_unreserved_ascii(decoded) {
          out.write_char(decoded.unsafe_to_char())
        } else {
          write_percent_encoded(out, decoded)
        }
        i = i + 3
        continue
      }
    }
    if code >= 128 {
      write_percent_encoded(out, code)
    } else {
      out.write_char(code.unsafe_to_char())
    }
    i = i + 1
  }
  out.to_string()
}

///|
fn has_malformed_percent_encoding(input : String) -> Bool {
  let chars = input.to_array()
  let mut i = 0
  while i < chars.length() {
    if chars[i] == '%' {
      if i + 2 >= chars.length() ||
        hex_value(chars[i + 1]) < 0 ||
        hex_value(chars[i + 2]) < 0 {
        return true
      }
      i = i + 3
    } else {
      i = i + 1
    }
  }
  false
}

///|
pub fn normalize_path_for_match(path : String) -> Result[String, RobotsError] {
  if has_malformed_percent_encoding(path) {
    Err(robots_error(Pattern, InvalidPercentEncoding, 0, 0, path))
  } else {
    let clean = if path.length() == 0 {
      "/"
    } else if path.has_prefix("/") {
      path
    } else {
      "/\{path}"
    }
    Ok(normalize_percent_encoding(clean))
  }
}

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

///|
fn rule_match_length(pattern : String) -> Int {
  let chars = pattern.to_array()
  let mut n = 0
  let mut i = 0
  while i < chars.length() {
    if chars[i] == '%' && i + 2 < chars.length() {
      n = n + 1
      i = i + 3
    } else {
      if chars[i] != '*' && chars[i] != '$' {
        n = n + 1
      }
      i = i + 1
    }
  }
  n
}

///|
fn pattern_exact_end(pattern : String) -> Bool {
  pattern.has_suffix("$")
}

///|
fn pattern_body(pattern : String) -> String {
  if pattern_exact_end(pattern) {
    let chars = pattern.to_array()
    String::from_array(chars[0:chars.length() - 1])
  } else {
    pattern
  }
}

///|
fn wildcard_match(
  pattern : Array[Char],
  p_index : Int,
  path : Array[Char],
  s_index : Int,
  exact_end : Bool,
) -> Bool {
  if p_index == pattern.length() {
    return if exact_end { s_index == path.length() } else { true }
  }
  if pattern[p_index] == '*' {
    for next = s_index; next <= path.length(); next = next + 1 {
      if wildcard_match(pattern, p_index + 1, path, next, exact_end) {
        return true
      }
    }
    false
  } else if s_index < path.length() && pattern[p_index] == path[s_index] {
    wildcard_match(pattern, p_index + 1, path, s_index + 1, exact_end)
  } else {
    false
  }
}

///|
pub fn pattern_matches(pattern : String, path : String) -> Bool {
  let normalized_pattern = normalize_rule_pattern_for_match(pattern)
  let body = pattern_body(normalized_pattern)
  wildcard_match(
    body.to_array(),
    0,
    path.to_array(),
    0,
    pattern_exact_end(normalized_pattern),
  )
}

///|
pub fn pattern_specificity(pattern : String) -> Int {
  rule_match_length(normalize_rule_pattern_for_match(pattern))
}