///|
type Ipv4addr Bytes

///|
type Ipv6addr Bytes

///|
type Ipaddr Bytes

///|
#external
type AddrInfoResults

///|
pub struct AddrInfo {
  family : Int
  socktype : Int
  protocol : Int
  addr : Ipaddr
  canonname : String?
}

///|
pub(open) trait ToIpaddr {
  ip_name(self : Self) -> String raise NetError
  to_socket_addr(self : Self) -> SocketAddr raise NetError
  to_ipaddr(self : Self) -> Ipaddr raise NetError
}

///|
trait RawPointer {
  is_null(self : Self) -> Bool
}

///|
pub(open) trait ToCString {
  to_cstring(self : Self) -> Bytes
}

///|
pub impl ToIpaddr for Ipaddr with to_ipaddr(self : Ipaddr) -> Ipaddr = "%identity"

///|
pub impl ToIpaddr for Ipv4addr with to_ipaddr(self : Ipv4addr) -> Ipaddr = "%identity"

///|
pub impl ToIpaddr for Ipv6addr with to_ipaddr(self : Ipv6addr) -> Ipaddr = "%identity"

///|
extern "C" fn addrinfo_is_null_pointer_ffi(ptr : AddrInfoResults) -> Bool = "moonbit_is_null_pointer"

///|
extern "C" fn addr_is_null_pointer_ffi(addr : Ipaddr) -> Bool = "moonbit_is_null_pointer"

///|
extern "C" fn bytes_is_null_pointer_ffi(bytes : Bytes) -> Bool = "moonbit_is_null_pointer"

///|
extern "C" fn ipv4_addrlen_ffi() -> Int = "moonbit_ipv4_addrlen_ffi"

///|
extern "C" fn ipv6_addrlen_ffi() -> Int = "moonbit_ipv6_addrlen_ffi"

///|
extern "C" fn get_ip_name_ffi(
  addr : Ipaddr,
  cb : (Int, Int, Int, Int, @c.Pointer[Byte]) -> Bool
) -> Int = "moonbit_get_ip_name_ffi"

///|
extern "C" fn get_ipv4_name_ffi(addr : Ipv4addr, bytes : Bytes) -> Int = "moonbit_get_ipv4_name_ffi"

///|
extern "C" fn get_ipv6_name_ffi(addr : Ipv6addr, bytes : Bytes) -> Int = "moonbit_get_ipv6_name_ffi"

///|
extern "C" fn create_sockaddr_storage_ffi() -> Ipaddr = "moonbit_create_sockaddr_storage_ffi"

///|
let _IP_V4_ADDRLEN : Int = ipv4_addrlen_ffi()

///|
let _IP_V6_ADDRLEN : Int = ipv6_addrlen_ffi()

///|
impl RawPointer for AddrInfoResults with is_null(self : AddrInfoResults) -> Bool {
  return addrinfo_is_null_pointer_ffi(self)
}

///|
impl RawPointer for Ipaddr with is_null(self : Ipaddr) -> Bool {
  return addr_is_null_pointer_ffi(self)
}

///|
impl RawPointer for Bytes with is_null(self : Bytes) -> Bool {
  return bytes_is_null_pointer_ffi(self)
}

///|
pub impl ToCString for String with to_cstring(self : String) -> Bytes {
  let buf = @buffer.new()
  @encoding.encode_to(self, buf, encoding=UTF8)
  buf.write_byte((0).to_byte()) // Null-terminate the string
  return buf.contents()
}

///|
pub impl ToIpaddr for Ipaddr with to_socket_addr(self : Ipaddr) -> SocketAddr raise NetError {
  let buffer = @buffer.new()
  let mut family : Int = -1
  let mut port : Int = 0
  let mut flowinfo : Int = 0
  let mut scope_id : Int = 0
  let status = get_ip_name_ffi(self, fn(
    family_val : Int,
    port_val : Int,
    flowinfo_val : Int,
    scope_id_val : Int,
    ip_str : @c.Pointer[Byte]
  ) {
    if ip_str.is_null() {
      return false
    }
    for i = 0; ip_str[i] != 0; i = i + 1 {
      buffer.write_byte(ip_str[i])
    }
    family = family_val
    port = port_val
    flowinfo = flowinfo_val
    scope_id = scope_id_val
    return true
  })
  guard status >= 0 else {
    raise NetError("Failed to get IP address name and port from ffi")
  }
  let decoder = @encoding.decoder(UTF8)
  let host = decoder.lossy_consume(buffer.contents())
  if family == _AF_INET6 {
    return SocketAddr::V6(
      host,
      port.to_uint16(),
      flowinfo=flowinfo.reinterpret_as_uint(),
      scope_id=scope_id.reinterpret_as_uint(),
    )
  } else if family == _AF_INET {
    return SocketAddr::V4(host, port.to_uint16())
  } else {
    raise NetError("Unsupported address family: \{family}")
  }
}

///|
pub impl ToIpaddr for Ipaddr with ip_name(self : Ipaddr) -> String raise NetError {
  match self.to_socket_addr() {
    V4(host, _) => host
    V6(host, _, ..) => host
  }
}

///|
pub impl ToIpaddr for Ipv4addr with to_socket_addr(self : Ipv4addr) -> SocketAddr raise NetError {
  self.to_ipaddr().to_socket_addr()
}

///|
pub impl ToIpaddr for Ipv4addr with ip_name(self : Ipv4addr) -> String raise NetError {
  self.to_ipaddr().ip_name()
}

///|
pub impl ToIpaddr for Ipv6addr with to_socket_addr(self : Ipv6addr) -> SocketAddr raise NetError {
  self.to_ipaddr().to_socket_addr()
}

///|
pub impl ToIpaddr for Ipv6addr with ip_name(self : Ipv6addr) -> String raise NetError {
  let bytes = Bytes::make(_IP_V6_ADDRLEN, 0)
  let status = get_ipv6_name_ffi(self, bytes)
  guard status >= 0 else {
    raise NetError("Failed to get IPv6 address name from ffi")
  }
  let buffer = @buffer.new()
  for i = 0; bytes[i] != 0; i = i + 1 {
    buffer.write_byte(bytes[i])
  }
  let decoder = @encoding.decoder(UTF8)
  return decoder.lossy_consume(buffer.contents())
}

///|
extern "C" fn getaddrinfo_make_ffi(
  hostname : Bytes,
  service : Bytes,
  ai_flags : Int,
  ai_family : Int,
  ai_socktype : Int,
  ai_protocol : Int
) -> AddrInfoResults = "moonbit_getaddrinfo_make_ffi"

///|
extern "C" fn addrinfo_results_iterate_ffi(
  addr_info : AddrInfoResults,
  cb : (Int, Int, Int, Int, Ipaddr, @c.Pointer[Byte]) -> Bool
) -> Bool = "moonbit_addrinfo_results_iterate_ffi"

///|
pub fn AddrInfoResults::iter(self : AddrInfoResults) -> Iter[AddrInfo] {
  Iter::new(fn(yield_ : (AddrInfo) -> IterResult) {
    if addrinfo_results_iterate_ffi(self, fn(
        _,
        family : Int,
        socktype : Int,
        protocol : Int,
        addr : Ipaddr,
        canonname : @c.Pointer[Byte]
      ) {
        let canonname = if canonname.is_null() {
          None
        } else {
          let buffer = @buffer.new()
          for i = 0; canonname[i] != 0; i = i + 1 {
            buffer.write_byte(canonname[i])
          }
          let decoder = @encoding.decoder(UTF8)
          Some(decoder.lossy_consume(buffer.contents()))
        }
        match yield_({ family, socktype, protocol, addr, canonname }) {
          IterEnd => true
          IterContinue => false
        }
      }) {
      return IterEnd
    } else {
      return IterContinue
    }
  })
}

///|
pub fn[H : ToCString, S : ToCString, TDO : ToDomain, ToT : ToType, ToP : ToProtocol] getaddrinfo(
  hostname : H,
  service : S,
  ai_flags : Int,
  ai_family : TDO,
  ai_socktype : ToT,
  ai_protocol : ToP
) -> AddrInfoResults raise NetError {
  let service = service.to_cstring()
  let addr_info = getaddrinfo_make_ffi(
    hostname.to_cstring(),
    service,
    ai_flags,
    ai_family.to_domain().inner(),
    ai_socktype.to_type().inner(),
    ai_protocol.to_protocol().inner(),
  )
  if addr_info.is_null() {
    raise NetError("Failed to get address info")
  }
  return addr_info
}