///|
pub(all) suberror StunError {
  InvalidMessage(String)
  IntegrityFailure
  UnknownRequiredAttribute(UInt16)
  TransactionTimedOut
  AgentClosed
  TransactionAlreadyExists
  TransactionNotFound
  CryptoUnavailable(String)
} derive(Debug, Eq)

///|
pub(all) enum MessageClass {
  Request
  Indication
  SuccessResponse
  ErrorResponse
} derive(Debug, Eq)

///|
pub(all) enum Method {
  Binding
  Allocate
  Refresh
  Send
  Data
  CreatePermission
  ChannelBind
  Unknown(UInt16)
} derive(Debug, Eq)

///|
pub(all) enum AttributeType {
  MappedAddress
  ChangeRequest
  SourceAddress
  ChangedAddress
  Username
  MessageIntegrity
  ErrorCode
  UnknownAttributes
  ChannelNumber
  Lifetime
  XorPeerAddress
  DataAttribute
  Realm
  Nonce
  XorRelayedAddress
  RequestedAddressFamily
  EvenPort
  RequestedTransport
  DontFragment
  MessageIntegritySha256
  PasswordAlgorithm
  Userhash
  XorMappedAddress
  ReservationToken
  Priority
  UseCandidate
  PaddingAttribute
  ResponsePort
  ConnectionId
  PasswordAlgorithms
  AlternateDomain
  Software
  AlternateServer
  CacheTimeout
  Fingerprint
  IceControlled
  IceControlling
  ResponseOrigin
  OtherAddress
  Origin
  NetworkCost
  UnknownAttribute(UInt16)
} derive(Debug, Eq)

///|
pub fn AttributeType::from_code(code : UInt16) -> AttributeType {
  match code.to_uint() {
    0x0001U => MappedAddress
    0x0003U => ChangeRequest
    0x0004U => SourceAddress
    0x0005U => ChangedAddress
    0x0006U => Username
    0x0008U => MessageIntegrity
    0x0009U => ErrorCode
    0x000aU => UnknownAttributes
    0x000cU => ChannelNumber
    0x000dU => Lifetime
    0x0012U => XorPeerAddress
    0x0013U => DataAttribute
    0x0014U => Realm
    0x0015U => Nonce
    0x0016U => XorRelayedAddress
    0x0017U => RequestedAddressFamily
    0x0018U => EvenPort
    0x0019U => RequestedTransport
    0x001aU => DontFragment
    0x001cU => MessageIntegritySha256
    0x001dU => PasswordAlgorithm
    0x001eU => Userhash
    0x0020U => XorMappedAddress
    0x0022U => ReservationToken
    0x0024U => Priority
    0x0025U => UseCandidate
    0x0026U => PaddingAttribute
    0x0027U => ResponsePort
    0x002aU => ConnectionId
    0x8002U => PasswordAlgorithms
    0x8003U => AlternateDomain
    0x8022U => Software
    0x8023U => AlternateServer
    0x8027U => CacheTimeout
    0x8028U => Fingerprint
    0x8029U => IceControlled
    0x802aU => IceControlling
    0x802bU => ResponseOrigin
    0x802cU => OtherAddress
    0x802fU => Origin
    0xc057U => NetworkCost
    _ => UnknownAttribute(code)
  }
}

///|
pub fn AttributeType::code(self : AttributeType) -> UInt16 {
  match self {
    MappedAddress => 0x0001
    ChangeRequest => 0x0003
    SourceAddress => 0x0004
    ChangedAddress => 0x0005
    Username => 0x0006
    MessageIntegrity => 0x0008
    ErrorCode => 0x0009
    UnknownAttributes => 0x000a
    ChannelNumber => 0x000c
    Lifetime => 0x000d
    XorPeerAddress => 0x0012
    DataAttribute => 0x0013
    Realm => 0x0014
    Nonce => 0x0015
    XorRelayedAddress => 0x0016
    RequestedAddressFamily => 0x0017
    EvenPort => 0x0018
    RequestedTransport => 0x0019
    DontFragment => 0x001a
    MessageIntegritySha256 => 0x001c
    PasswordAlgorithm => 0x001d
    Userhash => 0x001e
    XorMappedAddress => 0x0020
    ReservationToken => 0x0022
    Priority => 0x0024
    UseCandidate => 0x0025
    PaddingAttribute => 0x0026
    ResponsePort => 0x0027
    ConnectionId => 0x002a
    PasswordAlgorithms => 0x8002
    AlternateDomain => 0x8003
    Software => 0x8022
    AlternateServer => 0x8023
    CacheTimeout => 0x8027
    Fingerprint => 0x8028
    IceControlled => 0x8029
    IceControlling => 0x802a
    ResponseOrigin => 0x802b
    OtherAddress => 0x802c
    Origin => 0x802f
    NetworkCost => 0xc057
    UnknownAttribute(code) => code
  }
}

///|
pub fn AttributeType::is_comprehension_required(self : AttributeType) -> Bool {
  self.code().to_uint() < 0x8000U
}

///|
pub struct Attribute {
  attribute_type : AttributeType
  value : Bytes
  padding : Bytes
} derive(Debug, Eq)

///|
fn padding_length(value_length : Int) -> Int {
  (4 - value_length % 4) % 4
}

///|
fn validate_attribute(
  attribute_type : AttributeType,
  value : Bytes,
) -> Unit raise StunError {
  let length = value.length()
  if length > 0xffff {
    raise InvalidMessage("attribute value exceeds 65535 bytes")
  }
  match attribute_type {
    MessageIntegrity if length != 20 =>
      raise InvalidMessage("MESSAGE-INTEGRITY must contain 20 bytes")
    MessageIntegritySha256 if length < 16 || length > 32 =>
      raise InvalidMessage(
        "MESSAGE-INTEGRITY-SHA256 must contain 16 through 32 bytes",
      )
    Priority if length != 4 =>
      raise InvalidMessage("32-bit STUN attribute has invalid length \{length}")
    Fingerprint if length != 4 =>
      raise InvalidMessage("32-bit STUN attribute has invalid length \{length}")
    Lifetime if length != 4 =>
      raise InvalidMessage("32-bit STUN attribute has invalid length \{length}")
    RequestedTransport if length != 4 =>
      raise InvalidMessage("32-bit STUN attribute has invalid length \{length}")
    ChangeRequest if length != 4 =>
      raise InvalidMessage("32-bit STUN attribute has invalid length \{length}")
    RequestedAddressFamily if length != 4 =>
      raise InvalidMessage("32-bit STUN attribute has invalid length \{length}")
    CacheTimeout if length != 4 =>
      raise InvalidMessage("32-bit STUN attribute has invalid length \{length}")
    ConnectionId if length != 4 =>
      raise InvalidMessage("32-bit STUN attribute has invalid length \{length}")
    NetworkCost if length != 4 =>
      raise InvalidMessage("32-bit STUN attribute has invalid length \{length}")
    IceControlled | IceControlling | ReservationToken if length != 8 =>
      raise InvalidMessage("64-bit STUN attribute has invalid length \{length}")
    UseCandidate | DontFragment if length != 0 =>
      raise InvalidMessage("flag STUN attribute must be empty")
    ChannelNumber if length != 4 =>
      raise InvalidMessage("CHANNEL-NUMBER must contain 4 bytes")
    ResponsePort if length != 2 =>
      raise InvalidMessage("RESPONSE-PORT must contain 2 bytes")
    EvenPort if length != 1 =>
      raise InvalidMessage("EVEN-PORT must contain 1 byte")
    UnknownAttributes if length % 2 != 0 =>
      raise InvalidMessage("UNKNOWN-ATTRIBUTES must contain 16-bit values")
    ErrorCode if length < 4 =>
      raise InvalidMessage("ERROR-CODE must contain at least 4 bytes")
    MappedAddress
    | XorMappedAddress
    | XorPeerAddress
    | XorRelayedAddress
    | AlternateServer
    | SourceAddress
    | ChangedAddress
    | ResponseOrigin
    | OtherAddress if length != 8 && length != 20 =>
      raise InvalidMessage("STUN address attribute must contain 8 or 20 bytes")
    _ => ()
  }
}

///|
pub fn Attribute::new(
  attribute_type~ : AttributeType,
  value~ : Bytes,
) -> Attribute raise StunError {
  validate_attribute(attribute_type, value)
  {
    attribute_type,
    value,
    padding: Bytes::make(padding_length(value.length()), 0),
  }
}

///|
fn Attribute::from_wire(
  attribute_type : AttributeType,
  value : Bytes,
  padding : Bytes,
) -> Attribute raise StunError {
  validate_attribute(attribute_type, value)
  if padding.length() != padding_length(value.length()) {
    raise InvalidMessage("attribute padding has invalid length")
  }
  { attribute_type, value, padding, }
}

///|
pub fn Attribute::attribute_type(self : Attribute) -> AttributeType {
  self.attribute_type
}

///|
pub fn Attribute::value(self : Attribute) -> Bytes {
  self.value
}

///|
pub fn Attribute::username(value : String) -> Attribute raise StunError {
  text_attribute(Username, value, 513)
}

///|
pub fn Attribute::software(value : String) -> Attribute raise StunError {
  text_attribute(Software, value, 763)
}

///|
pub fn Attribute::realm(value : String) -> Attribute raise StunError {
  text_attribute(Realm, value, 763)
}

///|
pub fn Attribute::nonce(value : String) -> Attribute raise StunError {
  text_attribute(Nonce, value, 763)
}

///|
fn text_attribute(
  attribute_type : AttributeType,
  value : String,
  maximum_length : Int,
) -> Attribute raise StunError {
  let value = @utf8.encode(value)
  if value.length() > maximum_length {
    raise InvalidMessage("text STUN attribute exceeds \{maximum_length} bytes")
  }
  Attribute::new(attribute_type~, value~)
}

///|
pub fn Attribute::priority(value : UInt) -> Attribute raise StunError {
  Attribute::new(
    attribute_type=Priority,
    value=Bytes::from_array([
      (value >> 24).to_byte(),
      (value >> 16).to_byte(),
      (value >> 8).to_byte(),
      value.to_byte(),
    ]),
  )
}

///|
fn u64_attribute(
  attribute_type : AttributeType,
  value : UInt64,
) -> Attribute raise StunError {
  Attribute::new(
    attribute_type~,
    value=Bytes::from_array([
      (value >> 56).to_byte(),
      (value >> 48).to_byte(),
      (value >> 40).to_byte(),
      (value >> 32).to_byte(),
      (value >> 24).to_byte(),
      (value >> 16).to_byte(),
      (value >> 8).to_byte(),
      value.to_byte(),
    ]),
  )
}

///|
pub fn Attribute::ice_controlled(value : UInt64) -> Attribute raise StunError {
  u64_attribute(IceControlled, value)
}

///|
pub fn Attribute::ice_controlling(value : UInt64) -> Attribute raise StunError {
  u64_attribute(IceControlling, value)
}

///|
pub fn Attribute::use_candidate() -> Attribute raise StunError {
  Attribute::new(attribute_type=UseCandidate, value=b"")
}

///|
pub fn Attribute::as_text(self : Attribute) -> String raise StunError {
  match self.attribute_type {
    Username | Software | Realm | Nonce | Origin | AlternateDomain =>
      @utf8.decode(self.value) catch {
        Malformed(_) =>
          raise InvalidMessage("text STUN attribute is not valid UTF-8")
      }
    _ => raise InvalidMessage("attribute does not contain text")
  }
}

///|
pub fn Attribute::as_u32(self : Attribute) -> UInt raise StunError {
  if self.value.length() != 4 {
    raise InvalidMessage("attribute does not contain a 32-bit value")
  }
  (self.value[0].to_uint() << 24) |
  (self.value[1].to_uint() << 16) |
  (self.value[2].to_uint() << 8) |
  self.value[3].to_uint()
}

///|
pub fn Attribute::as_u64(self : Attribute) -> UInt64 raise StunError {
  if self.value.length() != 8 {
    raise InvalidMessage("attribute does not contain a 64-bit value")
  }
  (self.value[0].to_uint64() << 56) |
  (self.value[1].to_uint64() << 48) |
  (self.value[2].to_uint64() << 40) |
  (self.value[3].to_uint64() << 32) |
  (self.value[4].to_uint64() << 24) |
  (self.value[5].to_uint64() << 16) |
  (self.value[6].to_uint64() << 8) |
  self.value[7].to_uint64()
}

///|
pub struct TransactionId(Bytes) derive(Debug, Eq, Hash)

///|
pub fn TransactionId::from_bytes(
  value : Bytes,
) -> TransactionId raise StunError {
  if value.length() != 12 {
    raise InvalidMessage("transaction id must contain 12 bytes")
  }
  TransactionId(value)
}

///|
pub fn TransactionId::as_bytes(self : TransactionId) -> Bytes {
  self.0
}

///|
pub struct Message {
  class : MessageClass
  stun_method : Method
  transaction_id : TransactionId
  attributes : Array[Attribute]
} derive(Debug, Eq)

///|
pub fn Message::new(
  class~ : MessageClass,
  stun_method~ : Method,
  transaction_id~ : TransactionId,
  attributes? : Array[Attribute] = [],
) -> Message {
  { class, stun_method, transaction_id, attributes: attributes.copy(), }
}

///|
pub fn Message::class(self : Message) -> MessageClass {
  self.class
}

///|
pub fn Message::stun_method(self : Message) -> Method {
  self.stun_method
}

///|
pub fn Message::transaction_id(self : Message) -> TransactionId {
  self.transaction_id
}

///|
pub fn Message::attributes(self : Message) -> Array[Attribute] {
  self.attributes.copy()
}

///|
pub fn Message::add_attribute(self : Message, attribute : Attribute) -> Unit {
  self.attributes.push(attribute)
}

///|
pub fn Message::first_attribute(
  self : Message,
  attribute_type : AttributeType,
) -> Attribute? {
  for attribute in self.attributes {
    if attribute.attribute_type == attribute_type {
      return Some(attribute)
    }
  }
  None
}

///|
pub fn Message::unknown_required_attributes(self : Message) -> Array[UInt16] {
  let result : Array[UInt16] = []
  for attribute in self.attributes {
    match attribute.attribute_type {
      UnknownAttribute(code) if code.to_uint() < 0x8000U => result.push(code)
      _ => ()
    }
  }
  result
}