///|
/// Deterministic RFC-order writer and exact-source preserve writer.
fn write_line(sb : StringBuilder, kind : String, value : String) -> Unit {
  sb.write_string(kind)
  sb.write_char('=')
  sb.write_string(value)
  sb.write_string("\r\n")
}

///|
fn origin_value(item : Origin) -> String {
  "\{item.username()} \{item.session_id()} \{item.session_version()} \{item.network_type()} \{item.address_type()} \{item.unicast_address()}"
}

///|
fn connection_value(item : Connection) -> String {
  let sb = StringBuilder()
  sb.write_string(item.network_type())
  sb.write_char(' ')
  sb.write_string(item.address_type())
  sb.write_char(' ')
  sb.write_string(item.address())
  match item.ttl() {
    Some(value) => {
      sb.write_char('/')
      sb.write_string(value.to_string())
    }
    None => ()
  }
  match item.address_count() {
    Some(value) => {
      sb.write_char('/')
      sb.write_string(value.to_string())
    }
    None => ()
  }
  sb.to_string()
}

///|
fn write_bandwidth(sb : StringBuilder, item : Bandwidth) -> Unit {
  write_line(sb, "b", "\{item.modifier()}:\{item.value()}")
}

///|
fn write_attribute(sb : StringBuilder, item : Attribute) -> Unit {
  match item.value() {
    None => write_line(sb, "a", ascii_lower(item.name()))
    Some(value) => write_line(sb, "a", "\{ascii_lower(item.name())}:\{value}")
  }
}

///|
fn write_media(sb : StringBuilder, media : MediaDescription) -> Unit {
  let port = if media.port_count() == 1 {
    media.port().to_string()
  } else {
    "\{media.port()}/\{media.port_count()}"
  }
  write_line(
    sb,
    "m",
    "\{ascii_lower(media.media())} \{port} \{media.protocol()} \{join_with(media.formats(), " ")}",
  )
  match media.information() {
    Some(value) => write_line(sb, "i", value)
    None => ()
  }
  for item in media.connections() {
    write_line(sb, "c", connection_value(item))
  }
  for item in media.bandwidths() {
    write_bandwidth(sb, item)
  }
  for item in media.attributes() {
    write_attribute(sb, item)
  }
}

///|
/// Re-emits the exact source bytes supplied to the parser.
pub fn write_preserve(document : SessionDescription) -> String {
  document.raw_source()
}

///|
/// Emits a deterministic CRLF document in RFC 8866 field order.
pub fn write_canonical(
  document : SessionDescription,
) -> Result[String, SdpError] {
  let origin_item = match document.origin() {
    None =>
      return Err(
        sdp_error(
          "writer", "missing-origin", 1, 1, "document", "cannot write a document without o=",
        ),
      )
    Some(value) => value
  }
  if document.session_name().length() == 0 {
    return Err(
      sdp_error(
        "writer", "empty-session-name", 1, 1, "document", "cannot write an empty s=",
      ),
    )
  }
  if document.timings().length() == 0 {
    return Err(
      sdp_error(
        "writer", "missing-timing", 1, 1, "document", "cannot write a document without t=",
      ),
    )
  }
  let sb = StringBuilder()
  write_line(sb, "v", document.version().to_string())
  write_line(sb, "o", origin_value(origin_item))
  write_line(sb, "s", document.session_name())
  match document.information() {
    Some(value) => write_line(sb, "i", value)
    None => ()
  }
  match document.uri() {
    Some(value) => write_line(sb, "u", value)
    None => ()
  }
  for value in document.emails() {
    write_line(sb, "e", value)
  }
  for value in document.phones() {
    write_line(sb, "p", value)
  }
  match document.connection() {
    Some(value) => write_line(sb, "c", connection_value(value))
    None => ()
  }
  for item in document.bandwidths() {
    write_bandwidth(sb, item)
  }
  for item in document.timings() {
    write_line(sb, "t", "\{item.start()} \{item.stop()}")
    for repeat in item.repeats() {
      write_line(sb, "r", repeat)
    }
  }
  match document.timezone_adjustments() {
    Some(value) => write_line(sb, "z", value)
    None => ()
  }
  match document.encryption_key() {
    Some(value) => write_line(sb, "k", value)
    None => ()
  }
  for item in document.attributes() {
    write_attribute(sb, item)
  }
  for media in document.media_descriptions() {
    write_media(sb, media)
  }
  Ok(sb.to_string())
}

///|
/// Parses and canonicalizes in one operation.
pub fn canonicalize(input : String) -> Result[String, SdpError] {
  match parse(input) {
    Err(error) => Err(error)
    Ok(document) => write_canonical(document)
  }
}

///|
/// Checks canonical idempotence without relying on array equality.
pub fn canonical_is_idempotent(input : String) -> Bool {
  match canonicalize(input) {
    Err(_) => false
    Ok(first) =>
      match canonicalize(first) {
        Err(_) => false
        Ok(second) => first == second
      }
  }
}