pub fn generate_derives(traits : Array[String], name : String, kind : String, generics : String, body : String, custom_derives : Array[DeriveDef]) -> String {
  let result = StringBuilder()
  for t in traits {
    let code = generate_derive_impl(t.trim().to_owned(), name, kind, generics, body, custom_derives)
    result.write_string(code)
    result.write_string("\n")
  }
  result.to_string()
}

fn generate_derive_impl(trait_name : String, name : String, kind : String, generics : String, body : String, custom_derives : Array[DeriveDef]) -> String {
  match trait_name {
    "Constructor" => generate_constructor(name, kind, generics, body)
    "Getters" => generate_getters(name, kind, generics, body)
    "EnumFromStr" => generate_enum_from_str(name, kind, generics, body)
    _ => {
      for d in custom_derives {
        if d.name == trait_name {
          return expand_derive_template(d.template, name, kind, generics, body)
        }
      }
      "// unknown derive: " + trait_name + " for " + name + "\n"
    }
  }
}

fn generate_constructor(name : String, kind : String, generics : String, body : String) -> String {
  if kind != "struct" { return "// Constructor only supports structs\n" }
  let fields = parse_struct_fields(body)
  if fields.length() == 0 { return "// Constructor requires at least one field\n" }
  let full = type_with_generics(name, generics)
  let params = fields.map(fn(f) { f.0 + ": " + f.1 }).join(", ")
  let assigns = fields.map(fn(f) { f.0 }).join(", ")
  let gen_list = if generics.length() > 0 { generics } else { "" }
  "pub fn" + gen_list + " " + name + "::new(" + params + ") -> " + full + " { { " + assigns + " } }\n"
}

fn generate_getters(name : String, kind : String, generics : String, body : String) -> String {
  if kind != "struct" { return "// Getters only supports structs\n" }
  let fields = parse_struct_fields(body)
  if fields.length() == 0 { return "// Getters requires at least one field\n" }
  let full = type_with_generics(name, generics)
  let gen_list = if generics.length() > 0 { generics } else { "" }
  let buf = StringBuilder()
  for f in fields {
    buf.write_string("pub fn" + gen_list + " " + name + "::" + f.0 + "(self: " + full + ") -> " + f.1 + " { self." + f.0 + " }\n")
  }
  buf.to_string()
}

fn pascal_case(s : String) -> String {
  if s.length() == 0 { return s }
  let first = s[0]
  let upper = if first >= ('a' : UInt16) && first <= ('z' : UInt16) {
    (first - 32 : UInt16)
  } else { first }
  let result = StringBuilder()
  result.write_char(upper.to_int().unsafe_to_char())
  if s.length() > 1 { result.write_string(s[1:].to_owned()) }
  result.to_string()
}

fn generate_enum_from_str(name : String, kind : String, generics : String, body : String) -> String {
  if kind != "enum" { return "// EnumFromStr only supports enums\n" }
  let variants = parse_enum_variants(body)
  let unit = variants.filter(fn(v) { v.field_types.length() == 0 })
  if unit.length() == 0 { return "// EnumFromStr requires at least one unit variant\n" }
  let full = type_with_generics(name, generics)
  let buf = StringBuilder()
  let gen_list = if generics.length() > 0 { generics } else { "" }
  buf.write_string("pub fn" + gen_list + " " + name + "::from_string(s: String) -> " + full + "? {\n")
  buf.write_string("  match s {\n")
  for v in unit {
    buf.write_string("    \"" + v.name + "\" => Some(" + name + "::" + v.name + ")\n")
  }
  buf.write_string("    _ => None\n")
  buf.write_string("  }\n")
  buf.write_string("}\n")
  buf.to_string()
}

fn type_with_generics(name : String, generics : String) -> String {
  if generics.length() > 0 { name + generics } else { name }
}

fn parse_struct_fields(body : String) -> Array[(String, String)] {
  let fields : Array[(String, String)] = []
  let mut pos : Int = 0
  let len = body.length()
  pos = skip_ws(body, pos)
  if pos >= len { return fields }
  if body[pos] != ('{' : UInt16) { return fields }
  pos = pos + 1
  while pos < len {
    pos = skip_ws(body, pos)
    if pos >= len { break }
    if body[pos] == ('}' : UInt16) { break }
    let name_res = read_field_ident(body, pos)
    if name_res.0 == "" { pos = pos + 1; continue }
    pos = name_res.1
    pos = skip_ws(body, pos)
    if pos < len && body[pos] == (':' : UInt16) {
      pos = pos + 1
      pos = skip_ws(body, pos)
      let ty_res = read_type(body, pos)
      fields.push((name_res.0, ty_res.0))
      pos = ty_res.1
    }
    pos = skip_ws(body, pos)
    if pos < len && (body[pos] == (',' : UInt16) || body[pos] == (';' : UInt16)) { pos = pos + 1 }
  }
  fields
}

priv struct EnumVariant {
  name : String
  field_types : Array[String]
}

fn parse_enum_variants(body : String) -> Array[EnumVariant] {
  let variants : Array[EnumVariant] = []
  let mut pos : Int = 0
  let len = body.length()
  pos = skip_ws(body, pos)
  if pos >= len || body[pos] != ('{' : UInt16) { return variants }
  pos = pos + 1
  while pos < len {
    pos = skip_ws(body, pos)
    if pos >= len || body[pos] == ('}' : UInt16) { break }
    let name_res = read_field_ident(body, pos)
    if name_res.0 == "" { pos = pos + 1; continue }
    pos = name_res.1
    pos = skip_ws(body, pos)
    if pos < len && body[pos] == ('(' : UInt16) {
      let types = read_comma_types(body, pos)
      pos = types.1
      variants.push({ name: name_res.0, field_types: types.0 })
    } else if pos < len && body[pos] == ('{' : UInt16) {
      let fields_res = read_record_fields(body, pos)
      pos = fields_res.1
      let types = fields_res.0.map(fn(f) { f.1 })
      variants.push({ name: name_res.0, field_types: types })
    } else {
      variants.push({ name: name_res.0, field_types: [] })
    }
    pos = skip_ws(body, pos)
    if pos < len && body[pos] == (',' : UInt16) { pos = pos + 1 }
  }
  variants
}

fn read_comma_types(s : String, start : Int) -> (Array[String], Int) {
  let types : Array[String] = []
  if start >= s.length() { return (types, start) }
  if s[start] != ('(' : UInt16) { return (types, start) }
  let mut pos = start + 1
  let len = s.length()
  let mut depth = 1
  let mut type_start = pos
  while pos < len && depth > 0 {
    let c = s[pos]
    if c == ('(' : UInt16) || c == ('<' : UInt16) || c == ('[' : UInt16) { depth = depth + 1 }
    else if c == (')' : UInt16) || c == ('>' : UInt16) || c == (']' : UInt16) { depth = depth - 1 }
    else if c == (',' : UInt16) && depth == 1 {
      let t = s[type_start:pos].trim().to_owned()
      if t.length() > 0 { types.push(t) }
      type_start = pos + 1
    }
    pos = pos + 1
  }
  if type_start < pos - 1 {
    let t = s[type_start:pos - 1].trim().to_owned()
    if t.length() > 0 { types.push(t) }
  }
  (types, pos)
}

fn read_record_fields(s : String, start : Int) -> (Array[(String, String)], Int) {
  let fields : Array[(String, String)] = []
  if start >= s.length() { return (fields, start) }
  if s[start] != ('{' : UInt16) { return (fields, start) }
  let mut pos = start + 1
  let len = s.length()
  while pos < len {
    pos = skip_ws(s, pos)
    if pos >= len || s[pos] == ('}' : UInt16) { break }
    let name_res = read_field_ident(s, pos)
    if name_res.0 == "" { pos = pos + 1; continue }
    pos = name_res.1
    pos = skip_ws(s, pos)
    if pos < len && s[pos] == (':' : UInt16) {
      pos = pos + 1
      pos = skip_ws(s, pos)
      let ty_res = read_type(s, pos)
      fields.push((name_res.0, ty_res.0))
      pos = ty_res.1
    }
    if pos < len && (s[pos] == (',' : UInt16) || s[pos] == (';' : UInt16)) { pos = pos + 1 }
  }
  if pos < len { pos = pos + 1 }
  (fields, pos)
}

fn read_field_ident(s : String, start : Int) -> (String, Int) {
  let mut pos = start
  let len = s.length()
  if pos + 3 < len && s[pos:pos + 4].to_owned() == "mut " {
    pos = pos + 4
  }
  let start_pos = pos
  while pos < len {
    let c = s[pos]
    let is_valid = (c >= ('a' : UInt16) && c <= ('z' : UInt16)) ||
      (c >= ('A' : UInt16) && c <= ('Z' : UInt16)) ||
      c == ('_' : UInt16) ||
      (pos > start_pos && c >= ('0' : UInt16) && c <= ('9' : UInt16))
    if is_valid { pos = pos + 1 } else { break }
  }
  if pos == start_pos { ("", start) }
  else { (s[start_pos:pos].to_owned(), pos) }
}

fn read_type(s : String, start : Int) -> (String, Int) {
  let mut pos = start
  let len = s.length()
  let mut depth = 0
  while pos < len {
    let c = s[pos]
    if (c == (',' : UInt16) || c == (';' : UInt16) || c == ('}' : UInt16) || c == ('\n' : UInt16) || c == ('\r' : UInt16) || c == ('\t' : UInt16)) && depth == 0 { break }
    if c == ('<' : UInt16) || c == ('[' : UInt16) || c == ('(' : UInt16) { depth = depth + 1 }
    if c == ('>' : UInt16) || c == (']' : UInt16) || c == (')' : UInt16) { depth = depth - 1 }
    pos = pos + 1
  }
  (s[start:pos].trim().to_owned(), pos)
}