///|
pub(all) enum MediaDirection {
  SendRecv
  SendOnly
  RecvOnly
  Inactive
} derive(Debug, Eq)

///|
pub fn MediaDirection::to_string(self : MediaDirection) -> String {
  match self {
    SendRecv => "sendrecv"
    SendOnly => "sendonly"
    RecvOnly => "recvonly"
    Inactive => "inactive"
  }
}

///|
pub fn MediaDirection::parse(value : String) -> MediaDirection raise SdpError {
  match value {
    "sendrecv" => SendRecv
    "sendonly" => SendOnly
    "recvonly" => RecvOnly
    "inactive" => Inactive
    _ => raise InvalidSyntax("unknown RTP media direction")
  }
}

///|
pub(all) struct RtpCodecParameters {
  payload_type : Byte
  encoding_name : String
  clock_rate : UInt
  channels : UInt16
  fmtp : String
  rtcp_feedback : Array[String]
} derive(Debug, Eq)

///|
pub fn RtpCodecParameters::new(
  payload_type~ : Byte,
  encoding_name~ : String,
  clock_rate~ : UInt,
  channels? : UInt16 = 1,
  fmtp? : String = "",
  rtcp_feedback? : Array[String] = [],
) -> RtpCodecParameters raise SdpError {
  if payload_type > 127 ||
    encoding_name.is_empty() ||
    encoding_name.contains(" ") ||
    encoding_name.contains("/") ||
    clock_rate == 0U ||
    channels == 0 {
    raise InvalidSyntax("invalid RTP codec parameters")
  }
  if fmtp.contains("\r") ||
    fmtp.contains("\n") ||
    rtcp_feedback.any(value => {
      value.is_empty() || value.contains("\r") || value.contains("\n")
    }) {
    raise InvalidSyntax("invalid RTP codec feedback or FMTP")
  }
  {
    payload_type,
    encoding_name,
    clock_rate,
    channels,
    fmtp,
    rtcp_feedback: rtcp_feedback.copy(),
  }
}

///|
pub fn RtpCodecParameters::payload_type(self : RtpCodecParameters) -> Byte {
  self.payload_type
}

///|
pub fn RtpCodecParameters::encoding_name(self : RtpCodecParameters) -> String {
  self.encoding_name
}

///|
pub fn RtpCodecParameters::clock_rate(self : RtpCodecParameters) -> UInt {
  self.clock_rate
}

///|
pub fn RtpCodecParameters::channels(self : RtpCodecParameters) -> UInt16 {
  self.channels
}

///|
pub fn RtpCodecParameters::fmtp(self : RtpCodecParameters) -> String {
  self.fmtp
}

///|
pub fn RtpCodecParameters::rtcp_feedback(
  self : RtpCodecParameters,
) -> Array[String] {
  self.rtcp_feedback.copy()
}

///|
pub(all) struct RtpHeaderExtensionParameters {
  id : Byte
  uri : String
} derive(Debug, Eq)

///|
pub fn RtpHeaderExtensionParameters::new(
  id~ : Byte,
  uri~ : String,
) -> RtpHeaderExtensionParameters raise SdpError {
  if id == 0 ||
    uri.is_empty() ||
    uri.contains(" ") ||
    uri.contains("\r") ||
    uri.contains("\n") {
    raise InvalidSyntax("invalid RTP header extension parameters")
  }
  { id, uri, }
}

///|
pub fn RtpHeaderExtensionParameters::id(
  self : RtpHeaderExtensionParameters,
) -> Byte {
  self.id
}

///|
pub fn RtpHeaderExtensionParameters::uri(
  self : RtpHeaderExtensionParameters,
) -> String {
  self.uri
}

///|
pub(all) enum RidDirection {
  RidSend
  RidRecv
} derive(Debug, Eq)

///|
pub(all) struct RtpStreamParameters {
  ssrc : UInt?
  rtx_ssrc : UInt?
  rid : String?
  paused : Bool
  rid_direction : RidDirection
} derive(Debug, Eq)

///|
pub fn RtpStreamParameters::new(
  ssrc? : UInt,
  rtx_ssrc? : UInt,
  rid? : String,
  paused? : Bool = false,
  rid_direction? : RidDirection = RidSend,
) -> RtpStreamParameters raise SdpError {
  if ssrc is None && rid is None {
    raise InvalidSyntax("RTP stream requires an SSRC or RID")
  }
  if ssrc == Some(0U) || rtx_ssrc == Some(0U) {
    raise InvalidSyntax("RTP stream SSRC must be nonzero")
  }
  match (ssrc, rtx_ssrc) {
    (None, Some(_)) =>
      raise InvalidSyntax("RTP RTX SSRC requires a primary SSRC")
    (Some(primary), Some(rtx)) if primary == rtx =>
      raise InvalidSyntax("RTP primary and RTX SSRC must differ")
    _ => ()
  }
  match rid {
    Some(value) if value.is_empty() ||
      value.contains(" ") ||
      value.contains(";") ||
      value.contains(",") ||
      value.contains("\r") ||
      value.contains("\n") => raise InvalidSyntax("RTP stream RID is invalid")
    _ => ()
  }
  { ssrc, rtx_ssrc, rid, paused, rid_direction, }
}

///|
pub(all) struct RtpMediaParameters {
  kind : String
  mid : String
  direction : MediaDirection
  codecs : Array[RtpCodecParameters]
  header_extensions : Array[RtpHeaderExtensionParameters]
  stream_id : String?
  track_id : String?
  ssrc : UInt?
  rtx_ssrc : UInt?
  streams : Array[RtpStreamParameters]
  cname : String?
} derive(Debug, Eq)

///|
pub fn RtpMediaParameters::new(
  kind~ : String,
  mid~ : String,
  direction? : MediaDirection = SendRecv,
  codecs~ : Array[RtpCodecParameters],
  header_extensions? : Array[RtpHeaderExtensionParameters] = [],
  stream_id? : String,
  track_id? : String,
  ssrc? : UInt,
  rtx_ssrc? : UInt,
  streams? : Array[RtpStreamParameters] = [],
  cname? : String,
) -> RtpMediaParameters raise SdpError {
  if kind != "audio" && kind != "video" {
    raise InvalidSyntax("RTP media kind must be audio or video")
  }
  if mid.is_empty() || mid.contains(" ") || codecs.is_empty() {
    raise InvalidSyntax("RTP media section requires a MID and codecs")
  }
  if (stream_id is Some(_)) != (track_id is Some(_)) {
    raise InvalidSyntax("RTP media stream and track ids must appear together")
  }
  let extension_ids : Array[Byte] = []
  let extension_uris : Array[String] = []
  for extension in header_extensions {
    if extension_ids.contains(extension.id) ||
      extension_uris.contains(extension.uri) {
      raise InvalidSyntax("duplicate RTP header extension")
    }
    extension_ids.push(extension.id)
    extension_uris.push(extension.uri)
  }
  let normalized_streams = if streams.is_empty() {
    match ssrc {
      Some(_) => [RtpStreamParameters::new(ssrc?, rtx_ssrc?)]
      None => []
    }
  } else {
    streams.copy()
  }
  let mut normalized_ssrc = ssrc
  let mut normalized_rtx_ssrc = rtx_ssrc
  if !normalized_streams.is_empty() {
    match normalized_streams[0].ssrc {
      Some(value) =>
        match normalized_ssrc {
          Some(explicit) if explicit != value =>
            raise InvalidSyntax(
              "primary RTP SSRC disagrees with the first stream",
            )
          _ => normalized_ssrc = Some(value)
        }
      None => ()
    }
    match normalized_streams[0].rtx_ssrc {
      Some(value) =>
        match normalized_rtx_ssrc {
          Some(explicit) if explicit != value =>
            raise InvalidSyntax("RTX SSRC disagrees with the first stream")
          _ => normalized_rtx_ssrc = Some(value)
        }
      None => ()
    }
  }
  let stream_ssrcs : Array[UInt] = []
  let stream_rtx_ssrcs : Array[UInt] = []
  let stream_rids : Array[String] = []
  for stream in normalized_streams {
    match stream.ssrc {
      Some(value) => {
        if stream_ssrcs.contains(value) || stream_rtx_ssrcs.contains(value) {
          raise InvalidSyntax("duplicate RTP stream SSRC")
        }
        stream_ssrcs.push(value)
      }
      None => ()
    }
    match stream.rtx_ssrc {
      Some(value) => {
        if stream_ssrcs.contains(value) || stream_rtx_ssrcs.contains(value) {
          raise InvalidSyntax("duplicate RTP RTX SSRC")
        }
        stream_rtx_ssrcs.push(value)
      }
      None => ()
    }
    match stream.rid {
      Some(value) => {
        let key = (match stream.rid_direction {
            RidSend => "send:"
            RidRecv => "recv:"
          }) +
          value
        if stream_rids.contains(key) {
          raise InvalidSyntax("duplicate RTP stream RID")
        }
        stream_rids.push(key)
      }
      None => ()
    }
  }
  let has_stream_ssrc = normalized_streams.any(stream => stream.ssrc is Some(_))
  match (has_stream_ssrc, cname) {
    (true, None) =>
      raise InvalidSyntax("RTP SSRC and CNAME must appear together")
    _ => ()
  }
  match (normalized_ssrc, cname) {
    (Some(_), None) | (None, Some(_)) =>
      if !has_stream_ssrc {
        raise InvalidSyntax("RTP SSRC and CNAME must appear together")
      }
    _ => ()
  }
  match (normalized_ssrc, normalized_rtx_ssrc) {
    (None, Some(_)) =>
      raise InvalidSyntax("RTP RTX SSRC requires a primary SSRC")
    (Some(primary), Some(rtx)) if primary == rtx =>
      raise InvalidSyntax("RTP primary and RTX SSRC must differ")
    _ => ()
  }
  {
    kind,
    mid,
    direction,
    codecs: codecs.copy(),
    header_extensions: header_extensions.copy(),
    stream_id,
    track_id,
    ssrc: normalized_ssrc,
    rtx_ssrc: normalized_rtx_ssrc,
    streams: normalized_streams,
    cname,
  }
}

///|
pub fn RtpMediaParameters::kind(self : RtpMediaParameters) -> String {
  self.kind
}

///|
pub fn RtpMediaParameters::mid(self : RtpMediaParameters) -> String {
  self.mid
}

///|
pub fn RtpMediaParameters::direction(
  self : RtpMediaParameters,
) -> MediaDirection {
  self.direction
}

///|
pub fn RtpMediaParameters::codecs(
  self : RtpMediaParameters,
) -> Array[RtpCodecParameters] {
  self.codecs.copy()
}

///|
pub fn RtpMediaParameters::header_extensions(
  self : RtpMediaParameters,
) -> Array[RtpHeaderExtensionParameters] {
  self.header_extensions.copy()
}

///|
pub fn RtpMediaParameters::stream_id(self : RtpMediaParameters) -> String? {
  self.stream_id
}

///|
pub fn RtpMediaParameters::track_id(self : RtpMediaParameters) -> String? {
  self.track_id
}

///|
pub fn RtpMediaParameters::ssrc(self : RtpMediaParameters) -> UInt? {
  self.ssrc
}

///|
pub fn RtpMediaParameters::rtx_ssrc(self : RtpMediaParameters) -> UInt? {
  self.rtx_ssrc
}

///|
pub fn RtpMediaParameters::streams(
  self : RtpMediaParameters,
) -> Array[RtpStreamParameters] {
  self.streams.copy()
}

///|
pub fn RtpMediaParameters::cname(self : RtpMediaParameters) -> String? {
  self.cname
}

///|
fn parse_payload_type(value : StringView) -> Byte raise SdpError {
  let parsed = decimal_uint(value, "RTP payload type")
  if parsed > 127U {
    raise InvalidSyntax("RTP payload type exceeds 127")
  }
  parsed.to_byte()
}

///|
fn media_direction(media : MediaDescription) -> MediaDirection raise SdpError {
  let directions : Array[MediaDirection] = []
  for name in ["sendrecv", "sendonly", "recvonly", "inactive"] {
    if media.attribute(name) is Some(_) {
      directions.push(MediaDirection::parse(name))
    }
  }
  if directions.length() > 1 {
    raise InvalidSyntax("RTP media section has multiple directions")
  }
  if directions.is_empty() {
    SendRecv
  } else {
    directions[0]
  }
}

///|
fn parse_header_extensions(
  media : MediaDescription,
) -> Array[RtpHeaderExtensionParameters] raise SdpError {
  let extensions : Array[RtpHeaderExtensionParameters] = []
  for attribute in media.attributes("extmap") {
    guard attribute.value() is Some(value) &&
      value.split_once(" ") is Some((encoded_id, remainder)) else {
      raise InvalidSyntax("invalid RTP extmap attribute")
    }
    let id_token = match encoded_id.split_once("/") {
      Some((id, direction)) => {
        ignore(MediaDirection::parse(direction.to_owned()))
        id
      }
      None => encoded_id
    }
    let id_value = decimal_uint(id_token, "RTP extension id")
    if id_value == 0U || id_value > 255U {
      raise InvalidSyntax("RTP extension id is outside 1..255")
    }
    let uri = match remainder.split_once(" ") {
      Some((value, _)) => value.to_owned()
      None => remainder.to_owned()
    }
    extensions.push(
      RtpHeaderExtensionParameters::new(id=id_value.to_byte(), uri~),
    )
  }
  extensions
}

///|
fn keyed_attribute_values(
  media : MediaDescription,
  key : String,
  payload_type : Byte,
) -> Array[String] raise SdpError {
  let result : Array[String] = []
  for attribute in media.attributes(key) {
    guard attribute.value() is Some(value) else {
      raise InvalidSyntax("a=\{key} requires a value")
    }
    let tokens = value.split_once(" ")
    guard tokens is Some((encoded_payload_type, remainder)) else {
      raise InvalidSyntax("invalid a=\{key} value")
    }
    if parse_payload_type(encoded_payload_type) == payload_type {
      result.push(remainder.to_owned())
    }
  }
  result
}

///|
fn parse_rtp_codec(
  media : MediaDescription,
  format : String,
) -> RtpCodecParameters raise SdpError {
  let payload_type = parse_payload_type(format)
  let rtpmap = keyed_attribute_values(media, "rtpmap", payload_type)
  if rtpmap.length() != 1 {
    raise InvalidSyntax("dynamic RTP payload type requires one rtpmap")
  }
  let parts = rtpmap[0].split("/").to_array()
  if parts.length() < 2 || parts.length() > 3 {
    raise InvalidSyntax("invalid RTP rtpmap encoding")
  }
  let clock_rate = decimal_uint(parts[1], "RTP clock rate")
  let channels = if parts.length() == 3 {
    let value = decimal_uint(parts[2], "RTP channel count")
    if value == 0U || value > 0xffffU {
      raise InvalidSyntax("invalid RTP channel count")
    }
    value.to_uint16()
  } else {
    1
  }
  let fmtp_values = keyed_attribute_values(media, "fmtp", payload_type)
  if fmtp_values.length() > 1 {
    raise InvalidSyntax("duplicate RTP fmtp attribute")
  }
  RtpCodecParameters::new(
    payload_type~,
    encoding_name=parts[0].to_owned(),
    clock_rate~,
    channels~,
    fmtp=fmtp_values.get(0).unwrap_or(""),
    rtcp_feedback=keyed_attribute_values(media, "rtcp-fb", payload_type),
  )
}

///|
fn parse_stream_groups(
  media : MediaDescription,
) -> (Array[UInt], Map[UInt, UInt]) raise SdpError {
  let simulcast_ssrcs : Array[UInt] = []
  let fid : Map[UInt, UInt] = Map([])
  for attribute in media.attributes("ssrc-group") {
    guard attribute.value() is Some(value) else {
      raise InvalidSyntax("a=ssrc-group requires a value")
    }
    let tokens = space_tokens(value)
    if tokens.is_empty() {
      raise InvalidSyntax("invalid SDP SSRC group")
    }
    match tokens[0] {
      "FID" => {
        if tokens.length() != 3 {
          raise InvalidSyntax("invalid SDP FID SSRC group")
        }
        let primary = decimal_uint(tokens[1], "primary RTP SSRC")
        let rtx = decimal_uint(tokens[2], "RTX SSRC")
        if primary == 0U || rtx == 0U || primary == rtx || fid.contains(primary) {
          raise InvalidSyntax("invalid or duplicate SDP FID SSRC group")
        }
        fid[primary] = rtx
      }
      "SIM" => {
        if tokens.length() < 3 || !simulcast_ssrcs.is_empty() {
          raise InvalidSyntax("invalid or duplicate SDP SIM SSRC group")
        }
        for token in tokens[1:] {
          let ssrc = decimal_uint(token, "simulcast RTP SSRC")
          if ssrc == 0U || simulcast_ssrcs.contains(ssrc) {
            raise InvalidSyntax("duplicate simulcast RTP SSRC")
          }
          simulcast_ssrcs.push(ssrc)
        }
      }
      _ => ()
    }
  }
  (simulcast_ssrcs, fid)
}

///|
fn parse_simulcast_rids(
  media : MediaDescription,
) -> Array[(String, Bool, RidDirection)] raise SdpError {
  let declared : Array[(String, RidDirection)] = []
  for attribute in media.attributes("rid") {
    guard attribute.value() is Some(value) else {
      raise InvalidSyntax("a=rid requires a value")
    }
    let tokens = space_tokens(value)
    if tokens.length() < 2 || (tokens[1] != "send" && tokens[1] != "recv") {
      raise InvalidSyntax("invalid RTP RID attribute")
    }
    let rid = tokens[0]
    ignore(RtpStreamParameters::new(rid~))
    let rid_direction = if tokens[1] == "send" { RidSend } else { RidRecv }
    if declared.any(entry => entry.0 == rid && entry.1 == rid_direction) {
      raise InvalidSyntax("duplicate RTP RID attribute")
    }
    declared.push((rid, rid_direction))
  }
  let ordered : Array[(String, Bool, RidDirection)] = []
  let mut saw_send = false
  let mut saw_recv = false
  for attribute in media.attributes("simulcast") {
    guard attribute.value() is Some(value) &&
      value.split_once(" ") is Some((direction, layers)) else {
      raise InvalidSyntax("invalid simulcast attribute")
    }
    if direction != "send" && direction != "recv" {
      raise InvalidSyntax("invalid simulcast direction")
    }
    let rid_direction = if direction == "send" { RidSend } else { RidRecv }
    if (rid_direction == RidSend && saw_send) ||
      (rid_direction == RidRecv && saw_recv) {
      raise InvalidSyntax("duplicate simulcast direction")
    }
    if rid_direction == RidSend {
      saw_send = true
    } else {
      saw_recv = true
    }
    for layer in layers.split(";") {
      let alternatives = layer.split(",").to_array()
      if alternatives.is_empty() {
        raise InvalidSyntax("empty simulcast layer")
      }
      let encoded = alternatives[0]
      let stripped = encoded.strip_prefix("~")
      let paused = stripped is Some(_)
      let rid = stripped.unwrap_or(encoded).to_owned()
      if !declared.any(entry => entry.0 == rid && entry.1 == rid_direction) ||
        ordered.any(entry => entry.0 == rid && entry.2 == rid_direction) {
        raise InvalidSyntax("simulcast references an unknown or duplicate RID")
      }
      ordered.push((rid, paused, rid_direction))
    }
  }
  if ordered.is_empty() {
    for entry in declared {
      ordered.push((entry.0, false, entry.1))
    }
  }
  ordered
}

///|
fn parse_ssrc_attribute(
  media : MediaDescription,
) -> (UInt?, UInt?, Array[RtpStreamParameters], String?, String?, String?) raise SdpError {
  let (simulcast_ssrcs, fid) = parse_stream_groups(media)
  let all_ssrcs : Array[UInt] = []
  let mut cname : String? = None
  let mut stream_id : String? = None
  let mut track_id : String? = None
  for attribute in media.attributes("ssrc") {
    guard attribute.value() is Some(value) &&
      value.split_once(" ") is Some((encoded_ssrc, description)) else {
      raise InvalidSyntax("invalid SDP SSRC attribute")
    }
    let parsed_ssrc = decimal_uint(encoded_ssrc, "RTP SSRC")
    if parsed_ssrc == 0U {
      raise InvalidSyntax("RTP SSRC must be nonzero")
    }
    if !all_ssrcs.contains(parsed_ssrc) {
      all_ssrcs.push(parsed_ssrc)
    }
    match description.split_once(":") {
      Some(("cname", value)) =>
        match cname {
          Some(previous) if previous != value.to_owned() =>
            raise InvalidSyntax("RTP streams use different CNAME values")
          _ => cname = Some(value.to_owned())
        }
      Some(("msid", value)) => {
        let is_rtx = fid.values().contains(parsed_ssrc)
        if !is_rtx {
          let ids = space_tokens(value.to_owned())
          if ids.length() != 2 {
            raise InvalidSyntax("invalid SSRC msid attribute")
          }
          match (stream_id, track_id) {
            (Some(previous_stream), Some(previous_track)) =>
              if previous_stream != ids[0] || previous_track != ids[1] {
                raise InvalidSyntax(
                  "simulcast SSRCs use different media stream ids",
                )
              }
            _ => {
              stream_id = Some(ids[0])
              track_id = Some(ids[1])
            }
          }
        }
      }
      _ => ()
    }
  }
  match media.attribute("msid") {
    Some(attribute) =>
      match attribute.value() {
        Some(value) => {
          let ids = space_tokens(value)
          if ids.length() != 2 {
            raise InvalidSyntax("invalid media msid attribute")
          }
          stream_id = Some(ids[0])
          track_id = Some(ids[1])
        }
        None => raise InvalidSyntax("a=msid requires stream and track ids")
      }
    None => ()
  }
  let primary_ssrcs = if !simulcast_ssrcs.is_empty() {
    simulcast_ssrcs
  } else {
    let values : Array[UInt] = []
    for primary in fid.keys() {
      if !values.contains(primary) {
        values.push(primary)
      }
    }
    for ssrc in all_ssrcs {
      if !fid.values().contains(ssrc) && !values.contains(ssrc) {
        values.push(ssrc)
      }
    }
    values
  }
  for primary in primary_ssrcs {
    if !all_ssrcs.contains(primary) && !fid.contains(primary) {
      raise InvalidSyntax("SIM group references an unknown RTP SSRC")
    }
  }
  let rid_layers = parse_simulcast_rids(media)
  let streams : Array[RtpStreamParameters] = []
  for index = 0; index < primary_ssrcs.length(); index = index + 1 {
    let primary = primary_ssrcs[index]
    let rid = if index < rid_layers.length() {
      Some(rid_layers[index].0)
    } else {
      None
    }
    let paused = if index < rid_layers.length() {
      rid_layers[index].1
    } else {
      false
    }
    let rid_direction = if index < rid_layers.length() {
      rid_layers[index].2
    } else {
      RidSend
    }
    let rtx_ssrc = fid.get(primary)
    streams.push(
      RtpStreamParameters::new(
        ssrc=primary,
        rtx_ssrc?,
        rid?,
        paused~,
        rid_direction~,
      ),
    )
  }
  for index = primary_ssrcs.length()
      index < rid_layers.length()
      index = index + 1 {
    streams.push(
      RtpStreamParameters::new(
        rid=rid_layers[index].0,
        paused=rid_layers[index].1,
        rid_direction=rid_layers[index].2,
      ),
    )
  }
  let (first_ssrc, first_rtx) = match streams.get(0) {
    Some(stream) => (stream.ssrc, stream.rtx_ssrc)
    None => (None, None)
  }
  (first_ssrc, first_rtx, streams, cname, stream_id, track_id)
}

///|
pub fn SdpDocument::rtp_media_parameters(
  self : SdpDocument,
) -> Array[RtpMediaParameters] raise SdpError {
  let result : Array[RtpMediaParameters] = []
  for media in self.media_descriptions() {
    if media.media_kind() != "audio" && media.media_kind() != "video" {
      continue
    }
    if media.protocol() != "UDP/TLS/RTP/SAVPF" {
      raise UnsupportedAttribute("unsupported RTP media protocol")
    }
    let mid = attribute_value(self, media, "mid")
    let codecs : Array[RtpCodecParameters] = []
    for format in media.formats() {
      codecs.push(parse_rtp_codec(media, format))
    }
    let (ssrc, rtx_ssrc, streams, cname, stream_id, track_id) = parse_ssrc_attribute(
      media,
    )
    let header_extensions = parse_header_extensions(media)
    result.push(
      RtpMediaParameters::new(
        kind=media.media_kind(),
        mid~,
        direction=media_direction(media),
        codecs~,
        header_extensions~,
        stream_id?,
        track_id?,
        ssrc?,
        rtx_ssrc?,
        streams~,
        cname?,
      ),
    )
  }
  result
}

///|
pub fn SessionDescription::rtp_media_parameters(
  self : SessionDescription,
) -> Array[RtpMediaParameters] raise SdpError {
  self.document().rtp_media_parameters()
}