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

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

///|
fn is_identifier_char(ch : Char) -> Bool {
  is_identifier_start(ch) || is_digit_char(ch) || ch == ':'
}

///|
fn is_type_identifier_char(ch : Char) -> Bool {
  is_identifier_start(ch) || is_digit_char(ch)
}

///|
fn canonicalize_types(text : StringView) -> String {
  let builder = StringBuilder()
  let token = StringBuilder()
  for ch in text {
    if is_type_identifier_char(ch) {
      token.write_char(ch)
    } else {
      if !token.is_empty() {
        let value = token.to_string()
        match value {
          "Int" => builder.write_string("Int32")
          "UInt" => builder.write_string("UInt32")
          _ => builder.write_string(value)
        }
        token.reset()
      }
      builder.write_char(ch)
    }
  }
  if !token.is_empty() {
    let value = token.to_string()
    match value {
      "Int" => builder.write_string("Int32")
      "UInt" => builder.write_string("UInt32")
      _ => builder.write_string(value)
    }
  }
  builder.to_string()
}

///|
fn find_char(text : StringView, target : Char, start : Int32) -> Int32? {
  for index, ch in text.iter2() {
    if index >= start && ch == target {
      return Some(index)
    }
  }
  None
}

///|
fn substring(text : StringView, start : Int32, end : Int32) -> String {
  text.view(start_offset=start, end_offset=end).to_owned()
}

///|
fn normalize_whitespace(text : StringView) -> String {
  let builder = StringBuilder()
  let mut pending_space = false
  for ch in text {
    if ch.is_whitespace() {
      if !builder.is_empty() {
        pending_space = true
      }
    } else {
      if pending_space {
        builder.write_char(' ')
        pending_space = false
      }
      builder.write_char(ch)
    }
  }
  builder.to_string()
}

///|
fn skip_balanced(
  text : StringView,
  start : Int32,
  open : Char,
  close : Char,
) -> Int32 {
  let mut index = start
  let mut depth = 0
  let mut in_string = false
  let mut escaped = false
  while index < text.length() {
    let ch = text.get_char(index).unwrap()
    if in_string {
      if escaped {
        escaped = false
      } else if ch == '\\' {
        escaped = true
      } else if ch == '"' {
        in_string = false
      }
    } else if ch == '"' {
      in_string = true
    } else if ch == open {
      depth += 1
    } else if ch == close {
      depth -= 1
      if depth == 0 {
        return index + 1
      }
    }
    index += 1
  }
  text.length()
}

///|
fn find_matching_paren(text : StringView, open_index : Int32) -> Int32? {
  let end = skip_balanced(text, open_index, '(', ')')
  if end <= text.length() && end > open_index {
    Some(end - 1)
  } else {
    None
  }
}

///|
fn split_top_level(text : StringView) -> Array[String] {
  let result = []
  let mut start = 0
  let mut round = 0
  let mut square = 0
  let mut curly = 0
  let mut angle = 0
  let mut in_string = false
  let mut escaped = false
  for index, ch in text.iter2() {
    if in_string {
      if escaped {
        escaped = false
      } else if ch == '\\' {
        escaped = true
      } else if ch == '"' {
        in_string = false
      }
      continue
    }
    match ch {
      '"' => in_string = true
      '(' => round += 1
      ')' => round -= 1
      '[' => square += 1
      ']' => square -= 1
      '{' => curly += 1
      '}' => curly -= 1
      '<' => angle += 1
      '>' => angle -= 1
      ',' =>
        if round == 0 && square == 0 && curly == 0 && angle == 0 {
          result.push(substring(text, start, index).trim().to_owned())
          start = index + 1
        }
      _ => ()
    }
  }
  let tail = substring(text, start, text.length()).trim().to_owned()
  if !tail.is_empty() {
    result.push(tail)
  }
  result
}

///|
fn top_level_assignment_index(text : StringView) -> Int32? {
  let mut round = 0
  let mut square = 0
  let mut curly = 0
  let mut angle = 0
  for index, ch in text.iter2() {
    match ch {
      '(' => round += 1
      ')' => round -= 1
      '[' => square += 1
      ']' => square -= 1
      '{' => curly += 1
      '}' => curly -= 1
      '<' => angle += 1
      '>' => angle -= 1
      '=' =>
        if round == 0 && square == 0 && curly == 0 && angle == 0 {
          return Some(index)
        }
      _ => ()
    }
  }
  None
}

///|
fn declaration_depth_delta(text : StringView) -> Int32 {
  let mut delta = 0
  let mut in_string = false
  let mut escaped = false
  for ch in text {
    if in_string {
      if escaped {
        escaped = false
      } else if ch == '\\' {
        escaped = true
      } else if ch == '"' {
        in_string = false
      }
      continue
    }
    match ch {
      '"' => in_string = true
      '(' | '[' | '{' => delta += 1
      ')' | ']' | '}' => delta -= 1
      _ => ()
    }
  }
  delta
}

///|
fn json_escape(text : StringView) -> String {
  let builder = StringBuilder()
  for ch in text {
    match ch {
      '"' => builder.write_string("\\\"")
      '\\' => builder.write_string("\\\\")
      '\n' => builder.write_string("\\n")
      '\r' => builder.write_string("\\r")
      '\t' => builder.write_string("\\t")
      _ => builder.write_char(ch)
    }
  }
  builder.to_string()
}