///|
/// A single attribute parameter, for example `TYPE=cell` or a bare `PREF`.
pub struct Param {
  name : String
  values : Array[String]
} derive(Eq, Debug)

///|
/// A property value together with the parameters written on its own line.
pub struct Field {
  value : String
  params : Array[Param]
} derive(Eq, Debug)

///|
pub struct Contact {
  version : String
  full_name : String
  family_name : String
  given_name : String
  phones : Array[Field]
  emails : Array[Field]
} derive(Eq, Debug)

///|
pub struct ParseResult {
  contacts : Array[Contact]
  diagnostics : Array[String]
} derive(Eq, Debug)

///|
fn unfold(input : String) -> Array[String] {
  let lines : Array[String] = []
  let normalized = input
    .replace_all(old="\r\n", new="\n")
    .replace_all(old="\r", new="\n")
  for raw in normalized.split("\n") {
    if raw.has_prefix(" ") || raw.has_prefix("\t") {
      if !lines.is_empty() {
        let chars = raw.iter().to_array()
        let tail = StringBuilder()
        for index in 1.. String? {
  let chars = input.iter().to_array()
  let result = StringBuilder()
  let mut index = 0
  while index < chars.length() {
    if chars[index] == '\\' {
      if index + 1 == chars.length() {
        return None
      }
      match chars[index + 1] {
        'n' | 'N' => result.write_char('\n')
        '\\' => result.write_char('\\')
        ';' => result.write_char(';')
        ',' => result.write_char(',')
        _ => return None
      }
      index += 2
    } else {
      result.write_char(chars[index])
      index += 1
    }
  }
  Some(result.to_string())
}

///|
fn escape_text(input : String) -> String {
  let result = StringBuilder()
  for c in input.iter() {
    match c {
      '\n' => result.write_string("\\n")
      '\\' => result.write_string("\\\\")
      ';' => result.write_string("\\;")
      ',' => result.write_string("\\,")
      _ => result.write_char(c)
    }
  }
  result.to_string()
}

///|
/// Splits `input` at its first `separator` and keeps any later separator in the tail.
fn split_once(input : String, separator : String) -> (String, String?) {
  let parts = input.split(separator).to_array()
  if parts.length() < 2 {
    return (input, None)
  }
  let tail = StringBuilder()
  for index in 1.. 1 {
      tail.write_string(separator)
    }
    tail.write_string(parts[index].to_owned())
  }
  (parts[0].to_owned(), Some(tail.to_string()))
}

///|
/// Reads the property name and its parameters from the part before the first `:`.
fn parse_property_head(head : String) -> (String, Array[Param]) {
  let segments = head.split(";").to_array()
  if segments.length() == 0 {
    return ("", [])
  }
  let name = segments[0].trim().to_owned()
  let params : Array[Param] = []
  for index in 1..= 2 {
          values.push(text.replace_all(old="\"", new=""))
        } else {
          for piece in text.split(",") {
            values.push(piece.trim().to_owned())
          }
        }
      }
      None => ()
    }
    params.push({ name: param_name.trim().to_owned(), values, })
  }
  (name, params)
}

///|
/// Writes one property line, parameters included, followed by CRLF.
fn write_field(output : StringBuilder, name : String, field : Field) -> Unit {
  output.write_string(name)
  for param in field.params {
    output.write_string(";")
    output.write_string(param.name)
    if param.values.length() > 0 {
      output.write_string("=")
      output.write_string(param.values.join(","))
    }
  }
  output.write_string(":")
  output.write_string(escape_text(field.value))
  output.write_string("\r\n")
}

///|
/// ASCII-only upper casing, enough for parameter names and encoding values.
fn ascii_upper(input : String) -> String {
  let result = StringBuilder()
  for c in input.iter() {
    result.write_char(c.to_ascii_uppercase())
  }
  result.to_string()
}

///|
/// Values of the first parameter named `name`, which is expected in upper case.
fn param_values(params : Array[Param], name : String) -> Array[String]? {
  for param in params {
    if ascii_upper(param.name) == name {
      return Some(param.values)
    }
  }
  None
}

///|
fn has_quoted_printable_encoding(params : Array[Param]) -> Bool {
  match param_values(params, "ENCODING") {
    Some(values) => {
      for value in values {
        if ascii_upper(value) == "QUOTED-PRINTABLE" {
          return true
        }
      }
      false
    }
    None => false
  }
}

///|
/// True when the `CHARSET` parameter is absent or names UTF-8 / ASCII.
fn has_supported_charset(params : Array[Param]) -> Bool {
  match param_values(params, "CHARSET") {
    Some(values) => {
      if values.length() == 0 {
        return true
      }
      let name = ascii_upper(values[0])
      name == "UTF-8" || name == "US-ASCII" || name == "ASCII"
    }
    None => true
  }
}

///|
/// Drops the parameters whose meaning has already been applied to the stored value.
fn drop_consumed_params(
  params : Array[Param],
  keep_charset : Bool,
) -> Array[Param] {
  let kept : Array[Param] = []
  for param in params {
    let name = ascii_upper(param.name)
    if name == "ENCODING" {
      continue
    }
    if name == "CHARSET" && !keep_charset {
      continue
    }
    kept.push(param)
  }
  kept
}

///|
/// Reads a non-negative decimal integer, ignoring surrounding spaces.
fn parse_preference(input : String) -> Int? {
  let text = input.trim()
  if text.length() == 0 {
    return None
  }
  let zero = Char::to_int('0')
  let mut value = 0
  for c in text.iter() {
    let digit = Char::to_int(c) - zero
    if digit < 0 || digit > 9 {
      return None
    }
    value = value * 10 + digit
  }
  Some(value)
}

///|
/// Numeric `PREF` of a field, or `None` when it is absent or not a number.
fn preference_rank(field : Field) -> Int? {
  match param_values(field.params, "PREF") {
    Some(values) => {
      if values.length() == 0 {
        return None
      }
      parse_preference(values[0])
    }
    None => None
  }
}

///|
/// True when `left` should be ordered before `right`.
fn preference_before(left : Field, right : Field) -> Bool {
  match (preference_rank(left), preference_rank(right)) {
    (Some(a), Some(b)) => a < b
    (Some(_), None) => true
    (None, Some(_)) => false
    (None, None) => false
  }
}

///|
/// Orders multi-value fields by `PREF`: numeric preferences ascending, then every
/// field whose `PREF` is missing or not a number. Fields that share a rank keep
/// their original order, and the input array is left untouched.
pub fn sort_by_preference(fields : Array[Field]) -> Array[Field] {
  let used : Array[Bool] = []
  for _ in fields {
    used.push(false)
  }
  let ordered : Array[Field] = []
  while ordered.length() < fields.length() {
    let mut best = -1
    for index in 0.. String {
  let chars = input.iter().to_array()
  if chars.length() == 0 {
    return ""
  }
  let result = StringBuilder()
  let last = chars.length() - 1
  for index in 0.. Int? {
  let code = Char::to_int(c.to_ascii_uppercase())
  let zero = Char::to_int('0')
  let nine = Char::to_int('9')
  let upper_a = Char::to_int('A')
  let upper_f = Char::to_int('F')
  if code >= zero && code <= nine {
    return Some(code - zero)
  }
  if code >= upper_a && code <= upper_f {
    return Some(code - upper_a + 10)
  }
  None
}

///|
fn is_continuation(byte : Int) -> Bool {
  byte >= 0x80 && byte <= 0xBF
}

///|
/// Reads a byte, or `-1` past the end, so callers can check without a length test.
fn byte_at(bytes : Array[Int], index : Int) -> Int {
  if index < bytes.length() {
    bytes[index]
  } else {
    -1
  }
}

///|
fn utf8_bytes(code : Int) -> Array[Int] {
  if code < 0x80 {
    return [code]
  }
  if code < 0x800 {
    return [0xC0 | (code >> 6), 0x80 | (code & 0x3F)]
  }
  [0xE0 | (code >> 12), 0x80 | ((code >> 6) & 0x3F), 0x80 | (code & 0x3F)]
}

///|
/// Builds a `Char` from a code unit whose range the caller has already checked.
/// Surrogate halves are valid `Char` values, so the safe conversion is not enough.
fn char_of(code : Int) -> Char {
  code.unsafe_to_char()
}

///|
/// Decodes UTF-8 bytes and reports whether every sequence was well formed.
fn decode_utf8(bytes : Array[Int]) -> (String, Bool) {
  let result = StringBuilder()
  let replacement = char_of(0xFFFD)
  let mut index = 0
  let mut ok = true
  while index < bytes.length() {
    let first = bytes[index]
    if first < 0x80 {
      result.write_char(char_of(first))
      index += 1
    } else if first < 0xC2 {
      ok = false
      result.write_char(replacement)
      index += 1
    } else if first <= 0xDF {
      let second = byte_at(bytes, index + 1)
      if is_continuation(second) {
        let code = ((first - 0xC0) << 6) + (second - 0x80)
        result.write_char(char_of(code))
        index += 2
      } else {
        ok = false
        result.write_char(replacement)
        index += 1
      }
    } else if first <= 0xEF {
      let second = byte_at(bytes, index + 1)
      let third = byte_at(bytes, index + 2)
      let second_ok = is_continuation(second)
      let third_ok = is_continuation(third)
      if second_ok && third_ok {
        let lead = (first - 0xE0) << 12
        let middle = (second - 0x80) << 6
        let code = lead + middle + (third - 0x80)
        result.write_char(char_of(code))
        index += 3
      } else {
        ok = false
        result.write_char(replacement)
        index += 1
      }
    } else if first <= 0xF4 {
      let second = byte_at(bytes, index + 1)
      let third = byte_at(bytes, index + 2)
      let fourth = byte_at(bytes, index + 3)
      let second_ok = is_continuation(second)
      let third_ok = is_continuation(third)
      let fourth_ok = is_continuation(fourth)
      if second_ok && third_ok && fourth_ok {
        let lead = (first - 0xF0) << 18
        let high = (second - 0x80) << 12
        let middle = (third - 0x80) << 6
        let code = lead + high + middle + (fourth - 0x80)
        let value = code - 0x10000
        let unit_high = 0xD800 + (value >> 10)
        let unit_low = 0xDC00 + (value & 0x3FF)
        result.write_char(char_of(unit_high))
        result.write_char(char_of(unit_low))
        index += 4
      } else {
        ok = false
        result.write_char(replacement)
        index += 1
      }
    } else {
      ok = false
      result.write_char(replacement)
      index += 1
    }
  }
  (result.to_string(), ok)
}

///|
/// Decodes one quoted-printable value. `false` means a `=XX` sequence was malformed
/// or the decoded bytes were not valid UTF-8.
fn decode_quoted_printable(input : String) -> (String, Bool) {
  let chars = input.iter().to_array()
  let bytes : Array[Int] = []
  let mut index = 0
  let mut ok = true
  while index < chars.length() {
    let current = chars[index]
    let mut step = 1
    if current == '=' {
      let has_two = index + 2 < chars.length()
      if has_two {
        let upper = hex_value(chars[index + 1])
        let lower = hex_value(chars[index + 2])
        match (upper, lower) {
          (Some(high), Some(low)) => {
            bytes.push(high * 16 + low)
            step = 3
          }
          _ => {
            ok = false
            bytes.push(0x3D)
          }
        }
      } else {
        ok = false
        bytes.push(0x3D)
      }
    } else {
      let code = Char::to_int(current)
      if code < 0x80 {
        bytes.push(code)
      } else {
        for byte in utf8_bytes(code) {
          bytes.push(byte)
        }
      }
    }
    index += step
  }
  let (text, valid) = decode_utf8(bytes)
  (text, ok && valid)
}

///|
fn name_parts(input : String) -> Array[String]? {
  let parts : Array[String] = []
  let mut current = StringBuilder()
  let chars = input.iter().to_array()
  let mut index = 0
  while index < chars.length() {
    if chars[index] == '\\' && index + 1 < chars.length() {
      current.write_char(chars[index])
      current.write_char(chars[index + 1])
      index += 2
    } else if chars[index] == ';' {
      parts.push(current.to_string())
      current = StringBuilder()
      index += 1
    } else {
      current.write_char(chars[index])
      index += 1
    }
  }
  parts.push(current.to_string())
  if parts.length() < 2 {
    return None
  }
  let decoded : Array[String] = []
  for part in parts {
    match unescape_text(part) {
      Some(value) => decoded.push(value)
      None => return None
    }
  }
  Some(decoded)
}

///|
pub fn parse(input : String) -> ParseResult {
  let contacts : Array[Contact] = []
  let diagnostics : Array[String] = []
  let lines = unfold(input)
  let mut active = false
  let mut version = ""
  let mut full_name = ""
  let mut family_name = ""
  let mut given_name = ""
  let mut phones : Array[Field] = []
  let mut emails : Array[Field] = []
  let mut line_number = 0
  let mut index = 0
  while index < lines.length() {
    let line = lines[index]
    index += 1
    line_number += 1
    if line == "BEGIN:VCARD" {
      if active {
        diagnostics.push("nested-card:" + line_number.to_string())
      }
      active = true
      version = ""
      full_name = ""
      family_name = ""
      given_name = ""
      phones = []
      emails = []
      continue
    }
    if line == "END:VCARD" {
      if !active {
        diagnostics.push("unexpected-end:" + line_number.to_string())
      } else {
        let mut valid = true
        if version != "3.0" && version != "4.0" {
          diagnostics.push("invalid-version:" + line_number.to_string())
          valid = false
        }
        if full_name == "" {
          diagnostics.push("missing-fn:" + line_number.to_string())
          valid = false
        }
        if valid {
          contacts.push({
            version,
            full_name,
            family_name,
            given_name,
            phones,
            emails,
          })
        }
      }
      active = false
      continue
    }
    if line == "" {
      continue
    }
    if !active {
      diagnostics.push("outside-card:" + line_number.to_string())
      continue
    }
    let property_line = line_number
    let (head, maybe_raw) = split_once(line, ":")
    let raw = match maybe_raw {
      Some(text) => text
      None => {
        diagnostics.push("invalid-line:" + line_number.to_string())
        continue
      }
    }
    let (key, params) = parse_property_head(head)
    let mut field_params = params
    let mut text = raw
    if has_quoted_printable_encoding(params) {
      let supported = has_supported_charset(params)
      if !supported {
        diagnostics.push("unsupported-charset:" + property_line.to_string())
      }
      while text.has_suffix("=") && index < lines.length() {
        text = drop_last(text) + lines[index]
        index += 1
        line_number += 1
      }
      let (decoded, ok) = decode_quoted_printable(text)
      if !ok {
        diagnostics.push(
          "invalid-quoted-printable:" + property_line.to_string(),
        )
      }
      text = decoded
      field_params = drop_consumed_params(params, supported)
    }
    if key == "N" {
      match name_parts(text) {
        Some(names) => {
          family_name = names[0]
          given_name = names[1]
        }
        None => diagnostics.push("invalid-n:" + property_line.to_string())
      }
      continue
    }
    let value = match unescape_text(text) {
      Some(inner) => inner
      None => {
        diagnostics.push("invalid-escape:" + property_line.to_string())
        continue
      }
    }
    match key {
      "VERSION" => version = value
      "FN" => full_name = value
      "TEL" => if value != "" { phones.push({ value, params: field_params, }) }
      "EMAIL" =>
        if value != "" {
          emails.push({ value, params: field_params, })
        }
      _ => ()
    }
  }
  if active {
    diagnostics.push("unclosed-card")
  }
  { contacts, diagnostics, }
}

///|
pub fn format(contact : Contact) -> String? {
  if (contact.version != "3.0" && contact.version != "4.0") ||
    contact.full_name == "" {
    return None
  }
  let output = StringBuilder()
  output.write_string("BEGIN:VCARD\r\nVERSION:")
  output.write_string(contact.version)
  output.write_string("\r\nFN:")
  output.write_string(escape_text(contact.full_name))
  output.write_string("\r\nN:")
  output.write_string(escape_text(contact.family_name))
  output.write_string(";")
  output.write_string(escape_text(contact.given_name))
  output.write_string(";;;\r\n")
  for phone in contact.phones {
    write_field(output, "TEL", phone)
  }
  for email in contact.emails {
    write_field(output, "EMAIL", email)
  }
  output.write_string("END:VCARD\r\n")
  Some(output.to_string())
}