///|
pub(all) enum ProxyVersion {
  V1
  V2
} derive(Eq, Debug)

///|
pub(all) enum Command {
  Local
  Proxy
} derive(Eq, Debug)

///|
pub(all) enum AddressFamily {
  Unspec
  Inet
  Inet6
  Unix
} derive(Eq, Debug)

///|
pub(all) enum Transport {
  Unspec
  Stream
  Datagram
} derive(Eq, Debug)

///|
pub(all) struct Ipv4Address {
  octets : FixedArray[Byte]
} derive(Debug)

///|
pub(all) struct Ipv6Address {
  octets : FixedArray[Byte]
} derive(Debug)

///|
pub fn Ipv4Address::new(
  octets : FixedArray[Byte],
) -> Result[Ipv4Address, ProxyError] {
  if octets.length() != 4 {
    Err(proxy_error(InvalidIpv4, 0, "IPv4 address must have exactly 4 octets"))
  } else {
    Ok({ octets, })
  }
}

///|
pub fn Ipv6Address::new(
  octets : FixedArray[Byte],
) -> Result[Ipv6Address, ProxyError] {
  if octets.length() != 16 {
    Err(proxy_error(InvalidIpv6, 0, "IPv6 address must have exactly 16 octets"))
  } else {
    Ok({ octets, })
  }
}

///|
pub(all) struct Ipv4Endpoints {
  source_address : Ipv4Address
  destination_address : Ipv4Address
  source_port : Int
  destination_port : Int
} derive(Debug)

///|
pub(all) struct Ipv6Endpoints {
  source_address : Ipv6Address
  destination_address : Ipv6Address
  source_port : Int
  destination_port : Int
} derive(Debug)

///|
pub(all) struct UnixEndpoints {
  source : Bytes
  destination : Bytes
} derive(Debug)

///|
pub(all) enum AddressBlock {
  NoAddress
  Ipv4(Ipv4Endpoints)
  Ipv6(Ipv6Endpoints)
  UnixAddress(UnixEndpoints)
  RawUnsupported(Bytes)
} derive(Debug)

///|
pub(all) struct RawTlv {
  type_code : Byte
  value : Bytes
} derive(Debug)

///|
pub(all) struct ProxyHeader {
  version : ProxyVersion
  command : Command
  family : AddressFamily
  transport : Transport
  address : AddressBlock
  tlvs : Array[RawTlv]
} derive(Debug)

///|
pub(all) struct DecodedFrame {
  header : ProxyHeader
  remaining : Bytes
  consumed : Int
} derive(Debug)

///|
fn valid_port(port : Int) -> Bool {
  port >= 0 && port <= 65535
}

///|
pub fn ProxyHeader::new(
  version : ProxyVersion,
  command : Command,
  family : AddressFamily,
  transport : Transport,
  address : AddressBlock,
  tlvs : Array[RawTlv],
) -> Result[ProxyHeader, ProxyError] {
  if command == Local && version == V1 {
    return Err(proxy_error(PolicyViolation, 0, "v1 has no LOCAL command"))
  }
  if command == Local {
    return Ok({
      version,
      command,
      family: Unspec,
      transport: Unspec,
      address: NoAddress,
      tlvs: [],
    })
  }
  if (family == Unspec && transport != Unspec) ||
    (family == Inet && transport == Unspec) ||
    (family == Inet6 && transport == Unspec) ||
    (family == Unix && transport == Unspec) {
    return Err(
      proxy_error(
        InvalidFamilyTransport,
        0,
        "address family requires STREAM or DGRAM",
      ),
    )
  }
  match (family, address) {
    (Unspec, NoAddress) =>
      Ok({ version, command, family, transport, address, tlvs })
    (Inet, Ipv4(endpoints)) =>
      if valid_port(endpoints.source_port) &&
        valid_port(endpoints.destination_port) {
        Ok({ version, command, family, transport, address, tlvs })
      } else {
        Err(proxy_error(InvalidPort, 0, "port must be in 0..65535"))
      }
    (Inet6, Ipv6(endpoints)) =>
      if valid_port(endpoints.source_port) &&
        valid_port(endpoints.destination_port) {
        Ok({ version, command, family, transport, address, tlvs })
      } else {
        Err(proxy_error(InvalidPort, 0, "port must be in 0..65535"))
      }
    (Unix, UnixAddress(endpoints)) =>
      if endpoints.source.length() == 108 &&
        endpoints.destination.length() == 108 {
        Ok({ version, command, family, transport, address, tlvs })
      } else {
        Err(proxy_error(InvalidLength, 0, "UNIX fields must be 108 bytes each"))
      }
    (Unspec, RawUnsupported(_)) =>
      Ok({ version, command, family, transport, address, tlvs })
    _ =>
      Err(
        proxy_error(
          InvalidFamilyTransport,
          0,
          "family and address block disagree",
        ),
      )
  }
}