///|
fn convert_args(args : String) -> Map[String, String] {
  let params : Map[String, String] = Map([])
  let parts = args.split(",")
  let mut current_key = None
  for part in parts {
    let key_value = part.split("=").collect()
    if key_value.length() == 2 {
      let key = key_value[0].trim()
      let value = key_value[1].trim()
      if key.is_empty() || value.is_empty() {
        current_key = None
        continue
      }
      params[key.to_owned()] = value.to_owned()
      current_key = Some(key.to_owned())
    } else if key_value.length() == 1 {
      let value = part.trim()
      if !value.is_empty() {
        if current_key is Some(key) {
          params[key] = "\{params.get(key).unwrap()},\{value}"
        }
      }
    }
  }
  return params
}

///|
test "convert_args preserves comma-separated derive values" {
  let args = convert_args(
    "json=true,derive=Debug,Eq,Hash,Compare,Arbitrary,username=moon",
  )
  inspect(
    args.get("derive").unwrap(),
    content="Debug,Eq,Hash,Compare,Arbitrary",
  )
  inspect(args.get("username").unwrap(), content="moon")
}

///|
fn title(s : String) -> String {
  let sb = StringBuilder()
  let mut is_word_start = true
  for c in s {
    if c == ' ' {
      sb.write_char(' ')
      is_word_start = true
    } else if is_word_start {
      sb.write_char(c.to_ascii_uppercase())
      is_word_start = false
    } else {
      sb.write_char(c)
    }
  }
  sb.to_string()
}

///|
test "title" {
  inspect(title("hello world"), content="Hello World")
  inspect(title("the quick brown fox"), content="The Quick Brown Fox")
  inspect(title("a b c"), content="A B C")
}

///|
test "title/edge_cases" {
  inspect(title(""), content="")
  inspect(title(" "), content=" ")
  inspect(title("  "), content="  ")
  inspect(title("hello"), content="Hello")
}

///|
test "title/special_characters" {
  inspect(title("hello  world UK"), content="Hello  World UK")
  inspect(title(" hello world"), content=" Hello World")
  inspect(title("hello world "), content="Hello World ")
  inspect(title("123 abc"), content="123 Abc")
}

///|
fn to_camel_case(s : String) -> String {
  let b = StringBuilder()
  let chars = s.to_array()
  let len = chars.length()
  let mut i = 0
  while i < len {
    let c = chars[i]
    if (c == '.' || c == '_') &&
      i + 1 < len &&
      chars[i + 1].is_ascii_lowercase() {
      i += 1
      continue
    } else if c == '.' {
      b.write_char('_')
    } else if c == '_' && (i == 0 || chars[i - 1] == '.') {
      b.write_char('X')
    } else if c.is_ascii_digit() {
      b.write_char(c)
    } else {
      let upper_c = c.to_ascii_uppercase()
      b.write_char(upper_c)
      while i + 1 < len && chars[i + 1].is_ascii_lowercase() {
        i += 1
        b.write_char(chars[i])
      }
    }
    i += 1
  }
  return b.to_string()
}

///|
test "to_camel_case" {
  inspect(to_camel_case("hello_world"), content="HelloWorld")
  inspect(to_camel_case("test.proto"), content="TestProto")
  inspect(to_camel_case("_start"), content="Start")
  inspect(to_camel_case("simple"), content="Simple")
  inspect(to_camel_case("test123"), content="Test123")
  inspect(to_camel_case("xml_http_request"), content="XmlHttpRequest")
}

///|
fn pascal_to_snake(name : String) -> String {
  let keywords = [
    "as", "else", "extern", "fn", "fnalias", "if", "let", "const", "match", "using",
    "mut", "type", "typealias", "struct", "enum", "trait", "traitalias", "derive",
    "while", "break", "continue", "import", "return", "throw", "raise", "try", "catch",
    "pub", "priv", "readonly", "true", "false", "_", "test", "loop", "for", "in",
    "impl", "with", "guard", "async", "is", "suberror", "and", "letrec", "enumview",
    "noraise", "defer", "module", "move", "ref", "static", "super", "unsafe", "use",
    "where", "await", "dyn", "abstract", "do", "final", "macro", "override", "typeof",
    "virtual", "yield", "local", "method", "alias", "assert", "package", "recur",
    "using", "enumview", "isnot", "define", "downcast", "inherit", "member", "namespace",
    "static", "upcast", "use", "void", "lazy", "include", "mixin", "protected", "sealed",
    "constructor", "atomic", "volatile", "anyframe", "anytype", "asm", "await", "comptime",
    "errdefer", "export", "opaque", "orelse", "resume", "threadlocal", "unreachable",
    "dynclass", "dynobj", "dynrec", "var", "finally", "noasync", "init", "main",
  ]
  let new_name = StringBuilder()
  let upper = StringBuilder()
  let prev = '_'
  for rest = name[:], prev = prev {
    match (rest, prev) {
      (['A'..='Z' as c, .. rest], prev) => {
        if upper.is_empty() && prev != '_' {
          upper.write_char('_')
        }
        upper.write_char(c.to_ascii_lowercase())
        continue rest, c
      }
      ([c, .. rest], _) => {
        if !upper.is_empty() {
          new_name.write_string(upper.to_string())
          upper.reset()
        }
        new_name.write_char(c)
        continue rest, c
      }
      ([], _) => {
        if !upper.is_empty() {
          new_name.write_string(upper.to_string())
        }
        break
      }
    }
  }
  if keywords.contains(new_name.to_string()) {
    new_name.write_char('_')
  }
  return new_name.to_string()
}

///|
fn sizeFixed32() -> UInt {
  return 4U
}

///|
fn sizeFixed64() -> UInt {
  return 8U
}

///|
fn encode_tag(num : Int, typ : Int) -> UInt64 {
  (num.to_uint64() << 3) | (typ & 7).to_uint64()
}

///|
test "pascal_to_snake" {
  inspect(pascal_to_snake("PascalCase"), content="pascal_case")
  inspect(pascal_to_snake("SimpleWord"), content="simple_word")
  inspect(pascal_to_snake("XMLHttpRequest"), content="xmlhttp_request")
  inspect(pascal_to_snake("HTTPSConnection"), content="httpsconnection")
}

///|
test "pascal_to_snake/edge_cases" {
  inspect(pascal_to_snake(""), content="")
  inspect(pascal_to_snake("A"), content="a")
  inspect(pascal_to_snake("ABC"), content="abc")
  inspect(pascal_to_snake("lowercase"), content="lowercase")
}

///|
test "pascal_to_snake/keywords" {
  inspect(pascal_to_snake("Type"), content="type_")
  inspect(pascal_to_snake("Match"), content="match_")
  inspect(pascal_to_snake("New"), content="new")
}