///|
pub fn CandidateType::preference(self : CandidateType) -> UInt16 {
  match self {
    Host => 126
    PeerReflexive => 110
    ServerReflexive => 100
    Relay => 0
  }
}

///|
pub fn CandidateType::to_string(self : CandidateType) -> String {
  match self {
    Host => "host"
    ServerReflexive => "srflx"
    PeerReflexive => "prflx"
    Relay => "relay"
  }
}

///|
fn CandidateType::parse(value : String) -> CandidateType raise IceError {
  match value.to_lower() {
    "host" => Host
    "srflx" => ServerReflexive
    "prflx" => PeerReflexive
    "relay" => Relay
    _ => raise InvalidCandidate("unknown ICE candidate type")
  }
}

///|
pub fn TcpCandidateType::to_string(self : TcpCandidateType) -> String {
  match self {
    Active => "active"
    Passive => "passive"
    SimultaneousOpen => "so"
  }
}

///|
fn TcpCandidateType::parse(value : String) -> TcpCandidateType raise IceError {
  match value.to_lower() {
    "active" => Active
    "passive" => Passive
    "so" => SimultaneousOpen
    _ => raise InvalidCandidate("unknown ICE TCP candidate type")
  }
}

///|
fn valid_mdns_name(value : String) -> Bool {
  if value.length() > 253 ||
    value.is_empty() ||
    !value.to_lower().has_suffix(".local") {
    return false
  }
  for character in value.code_units() {
    if !((character >= 'a' && character <= 'z') ||
      (character >= 'A' && character <= 'Z') ||
      (character >= '0' && character <= '9') ||
      character == '-' ||
      character == '.') {
      return false
    }
  }
  true
}

///|
pub fn CandidateAddress::parse(
  value : String,
) -> CandidateAddress raise IceError {
  if value.to_lower().has_suffix(".local") {
    if !valid_mdns_name(value) {
      raise InvalidCandidate("invalid mDNS candidate name")
    }
    return MdnsName(value.to_lower())
  }
  let address = @transport.IpAddress::parse(value) catch {
    InvalidIpv6Length(_) | InvalidAddress(_) =>
      raise InvalidCandidate("invalid ICE candidate IP address")
  }
  IpAddress(address)
}

///|
pub fn CandidateAddress::to_string(self : CandidateAddress) -> String {
  match self {
    IpAddress(address) => address.to_string()
    MdnsName(name) => name
  }
}

///|
fn validate_token(value : String, context : String) -> Unit raise IceError {
  if value.is_empty() ||
    value.contains(" ") ||
    value.contains("\r") ||
    value.contains("\n") {
    raise InvalidCandidate("invalid ICE \{context}")
  }
}

///|
pub fn IceCandidate::new(
  foundation~ : String,
  component~ : UInt16,
  protocol~ : @transport.TransportProtocol,
  priority~ : UInt,
  address~ : CandidateAddress,
  port~ : UInt16,
  candidate_type~ : CandidateType,
  related_address? : CandidateAddress,
  related_port? : UInt16,
  tcp_type? : TcpCandidateType,
  extensions? : Array[(String, String)] = [],
) -> IceCandidate raise IceError {
  validate_token(foundation, "foundation")
  if component == 0 {
    raise InvalidCandidate("ICE component must be nonzero")
  }
  match address {
    MdnsName(name) if !valid_mdns_name(name) =>
      raise InvalidCandidate("invalid mDNS candidate name")
    _ => ()
  }
  match (protocol, tcp_type) {
    (Udp, Some(_)) =>
      raise InvalidCandidate("UDP candidate cannot have tcptype")
    (Tcp, None) => raise InvalidCandidate("TCP candidate requires tcptype")
    _ => ()
  }
  if port == 0 && !(protocol == Tcp && tcp_type == Some(Active)) {
    raise InvalidCandidate("ICE candidate port must be nonzero")
  }
  match (related_address, related_port) {
    (Some(_), None) | (None, Some(_)) =>
      raise InvalidCandidate(
        "related ICE address and port must appear together",
      )
    _ => ()
  }
  for extension in extensions {
    let (key, value) = extension
    validate_token(key, "extension key")
    validate_token(value, "extension value")
  }
  {
    foundation,
    component,
    protocol,
    priority,
    address,
    port,
    candidate_type,
    related_address,
    related_port,
    tcp_type,
    extensions: extensions.copy(),
  }
}

///|
pub fn IceCandidate::host(
  foundation~ : String,
  address : @transport.SocketAddress,
  local_preference? : UInt16 = 65535,
) -> IceCandidate raise IceError {
  IceCandidate::new(
    foundation~,
    component=1,
    protocol=Udp,
    priority=IceCandidate::priority_value(
      candidate_type=Host,
      local_preference~,
      component=1,
    ),
    address=IpAddress(address.address()),
    port=address.port(),
    candidate_type=Host,
  )
}

///|
pub fn IceCandidate::tcp_host(
  foundation~ : String,
  address : @transport.SocketAddress,
  tcp_type~ : TcpCandidateType,
  local_preference? : UInt16 = 65535,
) -> IceCandidate raise IceError {
  if tcp_type == Active && address.port() != 0 {
    raise InvalidCandidate("active ICE TCP host candidate must use port zero")
  }
  if tcp_type != Active && address.port() == 0 {
    raise InvalidCandidate(
      "passive ICE TCP host candidate requires a listening port",
    )
  }
  IceCandidate::new(
    foundation~,
    component=1,
    protocol=Tcp,
    priority=IceCandidate::priority_value(
      candidate_type=Host,
      local_preference~,
      component=1,
    ),
    address=IpAddress(address.address()),
    port=address.port(),
    candidate_type=Host,
    tcp_type~,
  )
}

///|
pub fn IceCandidate::priority_value(
  candidate_type~ : CandidateType,
  local_preference~ : UInt16,
  component~ : UInt16,
) -> UInt raise IceError {
  if component == 0 || component > 256 {
    raise InvalidCandidate("ICE component must be between 1 and 256")
  }
  (candidate_type.preference().to_uint() << 24) |
  (local_preference.to_uint() << 8) |
  (256U - component.to_uint())
}

///|
fn parse_uint64(value : String, context : String) -> UInt64 raise IceError {
  @string.from_str(value) catch {
    _ => raise InvalidCandidate("invalid ICE \{context}")
  }
}

///|
pub fn IceCandidate::parse(value : String) -> IceCandidate raise IceError {
  let value = match value.strip_prefix("candidate:") {
    Some(stripped) => stripped.to_owned()
    None => value
  }
  let tokens = value
    .split(" ")
    .filter(token => !token.is_empty())
    .map(token => token.to_owned())
    .to_array()
  if tokens.length() < 8 || tokens[6].to_lower() != "typ" {
    raise InvalidCandidate(
      "ICE candidate requires foundation, component, transport, priority, address, port, and type",
    )
  }
  let component = parse_uint64(tokens[1], "component")
  if component == 0UL || component > 0xffffUL {
    raise InvalidCandidate("ICE component is out of range")
  }
  let protocol : @transport.TransportProtocol = match tokens[2].to_lower() {
    "udp" => Udp
    "tcp" => Tcp
    _ => raise InvalidCandidate("unknown ICE transport protocol")
  }
  let priority = parse_uint64(tokens[3], "priority")
  if priority > 0xffffffffUL {
    raise InvalidCandidate("ICE priority is out of range")
  }
  let port = parse_uint64(tokens[5], "port")
  if port > 0xffffUL {
    raise InvalidCandidate("ICE port is out of range")
  }
  let candidate_type = CandidateType::parse(tokens[7])
  let mut related_address : CandidateAddress? = None
  let mut related_port : UInt16? = None
  let mut tcp_type : TcpCandidateType? = None
  let extensions : Array[(String, String)] = []
  if (tokens.length() - 8) % 2 != 0 {
    raise InvalidCandidate("ICE candidate extensions must be key-value pairs")
  }
  for index = 8; index < tokens.length(); index = index + 2 {
    let key = tokens[index].to_lower()
    let extension_value = tokens[index + 1]
    match key {
      "raddr" => {
        if related_address is Some(_) {
          raise InvalidCandidate("duplicate ICE related address")
        }
        related_address = Some(CandidateAddress::parse(extension_value))
      }
      "rport" => {
        if related_port is Some(_) {
          raise InvalidCandidate("duplicate ICE related port")
        }
        let parsed = parse_uint64(extension_value, "related port")
        if parsed > 0xffffUL {
          raise InvalidCandidate("ICE related port is out of range")
        }
        related_port = Some(parsed.to_uint16())
      }
      "tcptype" => {
        if tcp_type is Some(_) {
          raise InvalidCandidate("duplicate ICE TCP type")
        }
        tcp_type = Some(TcpCandidateType::parse(extension_value))
      }
      _ => extensions.push((tokens[index], extension_value))
    }
  }
  IceCandidate::new(
    foundation=tokens[0],
    component=component.to_uint16(),
    protocol~,
    priority=priority.to_uint(),
    address=CandidateAddress::parse(tokens[4]),
    port=port.to_uint16(),
    candidate_type~,
    related_address?,
    related_port?,
    tcp_type?,
    extensions~,
  )
}

///|
pub fn IceCandidate::foundation(self : IceCandidate) -> String {
  self.foundation
}

///|
pub fn IceCandidate::component(self : IceCandidate) -> UInt16 {
  self.component
}

///|
pub fn IceCandidate::protocol(
  self : IceCandidate,
) -> @transport.TransportProtocol {
  self.protocol
}

///|
pub fn IceCandidate::priority(self : IceCandidate) -> UInt {
  self.priority
}

///|
pub fn IceCandidate::address(self : IceCandidate) -> CandidateAddress {
  self.address
}

///|
pub fn IceCandidate::port(self : IceCandidate) -> UInt16 {
  self.port
}

///|
pub fn IceCandidate::candidate_type(self : IceCandidate) -> CandidateType {
  self.candidate_type
}

///|
pub fn IceCandidate::related_address(self : IceCandidate) -> CandidateAddress? {
  self.related_address
}

///|
pub fn IceCandidate::related_port(self : IceCandidate) -> UInt16? {
  self.related_port
}

///|
pub fn IceCandidate::tcp_type(self : IceCandidate) -> TcpCandidateType? {
  self.tcp_type
}

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

///|
pub fn IceCandidate::marshal(self : IceCandidate) -> String {
  let protocol = match self.protocol {
    Udp => "udp"
    Tcp => "tcp"
  }
  let mut result = self.foundation +
    " " +
    self.component.to_string() +
    " " +
    protocol +
    " " +
    self.priority.to_string() +
    " " +
    self.address.to_string() +
    " " +
    self.port.to_string() +
    " typ " +
    self.candidate_type.to_string()
  match (self.related_address, self.related_port) {
    (Some(address), Some(port)) =>
      result = result +
        " raddr " +
        address.to_string() +
        " rport " +
        port.to_string()
    _ => ()
  }
  match self.tcp_type {
    Some(tcp_type) => result = result + " tcptype " + tcp_type.to_string()
    None => ()
  }
  for extension in self.extensions {
    let (key, value) = extension
    result = result + " " + key + " " + value
  }
  result
}

///|
pub fn IceCandidate::to_sdp_attribute(self : IceCandidate) -> String {
  "candidate:" + self.marshal()
}

///|
pub fn IceCandidate::socket_address(
  self : IceCandidate,
) -> @transport.SocketAddress? {
  match self.address {
    IpAddress(address) =>
      Some(@transport.SocketAddress::new(address~, port=self.port))
    MdnsName(_) => None
  }
}

///|
pub fn IceCandidate::base_socket_address(
  self : IceCandidate,
) -> @transport.SocketAddress? {
  match self.candidate_type {
    ServerReflexive =>
      match (self.related_address, self.related_port) {
        (Some(IpAddress(address)), Some(port)) =>
          Some(@transport.SocketAddress::new(address~, port~))
        _ => self.socket_address()
      }
    Host | PeerReflexive | Relay => self.socket_address()
  }
}

///|
pub fn IceCandidate::resolve_mdns(
  self : IceCandidate,
  address : @transport.IpAddress,
) -> IceCandidate raise IceError {
  guard self.address is MdnsName(_) else {
    raise InvalidCandidate("candidate does not contain an mDNS name")
  }
  let related_address = self.related_address
  let related_port = self.related_port
  let tcp_type = self.tcp_type
  IceCandidate::new(
    foundation=self.foundation,
    component=self.component,
    protocol=self.protocol,
    priority=self.priority,
    address=IpAddress(address),
    port=self.port,
    candidate_type=self.candidate_type,
    related_address?,
    related_port?,
    tcp_type?,
    extensions=self.extensions,
  )
}

///|
pub fn IceCandidate::with_mdns_name(
  self : IceCandidate,
  name : String,
) -> IceCandidate raise IceError {
  if self.candidate_type != Host || self.protocol != Udp {
    raise InvalidCandidate(
      "only UDP host candidates can be advertised through mDNS",
    )
  }
  let related_address = self.related_address
  let related_port = self.related_port
  let tcp_type = self.tcp_type
  IceCandidate::new(
    foundation=self.foundation,
    component=self.component,
    protocol=self.protocol,
    priority=self.priority,
    address=CandidateAddress::parse(name),
    port=self.port,
    candidate_type=self.candidate_type,
    related_address?,
    related_port?,
    tcp_type?,
    extensions=self.extensions,
  )
}