// Routes Matcher - URL pattern matching
//

///|
/// Match URL to routes
pub fn match_url(url : String, routes : Array[CompiledRoutes]) -> RoutesMatch? {
  let (path, query) = parse_url_parts(url)
  for route in routes {
    if try_match_pattern(path, route) is Some(params) {
      return Some({ route, params, query, path, })
    }
  }
  None
}

///|
fn parse_url_parts(url : String) -> (String, Array[(String, String)]) {
  let chars = url.to_array()
  let mut query_start = -1
  for i, c in chars {
    if c == '?' {
      query_start = i
      break
    }
  }
  if query_start == -1 {
    return (url, [])
  }
  let path = String::from_array(chars[0:query_start])
  let query_str = String::from_array(chars[query_start + 1:])
  let query = parse_query(query_str)
  (path, query)
}

///|
fn parse_query(query : String) -> Array[(String, String)] {
  guard query != "" else { return [] }
  let result : Array[(String, String)] = []
  for pair in split_by(query, '&') {
    guard pair != "" else { continue }
    let (raw_key, raw_value_opt) = split_query_pair(pair)
    let key = decode_query_component(raw_key)
    if key == "" {
      continue
    }
    let value = match raw_value_opt {
      Some(raw_value) => decode_query_component(raw_value)
      None => ""
    }
    result.push((key, value))
  }
  result
}

///|
/// Split query pair by first '=' only.
/// "a=b=c" -> ("a", Some("b=c"))
fn split_query_pair(pair : String) -> (String, String?) {
  let chars = pair.to_array()
  let mut eq_index = -1
  for i, c in chars {
    if c == '=' {
      eq_index = i
      break
    }
  }
  if eq_index == -1 {
    (pair, None)
  } else {
    let key = String::from_array(chars[0:eq_index])
    let value = String::from_array(chars[eq_index + 1:])
    (key, Some(value))
  }
}

///|
/// Decode query component:
/// - '+' -> ' '
/// - percent encoding (%20 etc.) decoded as UTF-8 byte sequence
fn decode_query_component(s : String) -> String {
  let chars = s.to_array()
  let len = chars.length()
  let sb = StringBuilder(size_hint=len)
  let mut i = 0
  while i < len {
    let c = chars[i]
    let has_percent_triplet = c == '%' &&
      i + 2 < len &&
      is_hex_digit(chars[i + 1]) &&
      is_hex_digit(chars[i + 2])
    if c == '+' {
      sb.write_char(' ')
      i += 1
    } else if has_percent_triplet {
      let (decoded, next_i) = decode_percent_encoded_bytes(chars, i, len)
      sb.write_string(decoded)
      i = next_i
    } else {
      sb.write_char(c)
      i += 1
    }
  }
  sb.to_string()
}

///|
/// Decode contiguous percent-encoded bytes from `start`.
/// Returns (decoded_string, next_index_after_percent_run).
fn decode_percent_encoded_bytes(
  chars : Array[Char],
  start : Int,
  len : Int,
) -> (String, Int) {
  let bytes : Array[Byte] = []
  let mut i = start
  while i + 2 < len && chars[i] == '%' {
    let hi = chars[i + 1]
    let lo = chars[i + 2]
    if !(is_hex_digit(hi) && is_hex_digit(lo)) {
      break
    }
    let byte = hex_digit_to_int(hi) * 16 + hex_digit_to_int(lo)
    bytes.push(byte.to_byte())
    i += 3
  }
  (decode_percent_bytes(bytes), i)
}

///|
/// Decode bytes as UTF-8. If malformed, fall back to byte-wise chars.
fn decode_percent_bytes(bytes : Array[Byte]) -> String {
  let raw = Bytes::from_array(bytes)
  @utf8.decode(raw) catch {
    _ => {
      let fallback : Array[Char] = []
      for b in bytes {
        fallback.push(b.to_int().unsafe_to_char())
      }
      String::from_array(fallback)
    }
  }
}

///|
fn is_hex_digit(c : Char) -> Bool {
  (c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F')
}

///|
fn hex_digit_to_int(c : Char) -> Int {
  if c >= '0' && c <= '9' {
    c.to_int() - '0'.to_int()
  } else if c >= 'a' && c <= 'f' {
    c.to_int() - 'a'.to_int() + 10
  } else {
    c.to_int() - 'A'.to_int() + 10
  }
}

///|
fn try_match_pattern(
  path : String,
  route : CompiledRoutes,
) -> Array[(String, String)]? {
  let pattern_segments = split_path_segments(route.pattern)
  let path_segments = split_path_segments(path)

  // Handle catch-all routes
  match route.catch_all {
    Some(catch_all_info) => {
      // Pattern segment count excluding catch-all segment
      let base_pattern_len = pattern_segments.length() - 1
      let path_len = path_segments.length()

      // Required catch-all: needs at least one segment
      // Optional catch-all: zero segments is OK
      if catch_all_info.optional {
        if path_len < base_pattern_len {
          return None
        }
        // Required: path_len >= base_pattern_len + 1
      } else if path_len <= base_pattern_len {
        return None
      }

      // Base pattern matching
      let params : Array[(String, String)] = []
      let mut param_idx = 0
      for i in 0.. {
      // Normal matching (no catch-all)
      if pattern_segments.length() != path_segments.length() {
        return None
      }
      let params : Array[(String, String)] = []
      let mut param_idx = 0
      for i, pattern_seg in pattern_segments {
        let path_seg = path_segments[i]
        if starts_with_char(pattern_seg, ':') || is_bracket_param(pattern_seg) {
          if param_idx < route.param_names.length() {
            params.push((route.param_names[param_idx], path_seg))
            param_idx += 1
          } else {
            return None
          }
        } else if pattern_seg != path_seg {
          return None
        }
      }
      Some(params)
    }
  }
}

///|
fn starts_with_char(s : String, c : Char) -> Bool {
  s.has_prefix(c.to_string())
}

///|
/// Check if segment is a [id] style parameter (excluding catch-all)
fn is_bracket_param(s : String) -> Bool {
  s.has_prefix("[") &&
  s.has_suffix("]") &&
  !s.has_prefix("[...") &&
  !s.has_prefix("[[...")
}

///|
/// Join segments with "/"
fn join_segments(segments : Array[String]) -> String {
  if segments.is_empty() {
    return ""
  }
  let result = StringBuilder()
  for i, seg in segments {
    if i > 0 {
      result.write_string("/")
    }
    result.write_string(seg)
  }
  result.to_string()
}

///|
fn split_path_segments(path : String) -> Array[String] {
  split_by(path, '/').filter(fn(s) { s != "" })
}