///|
pub(all) enum AddrFamily {
  // Only IPv4 is supported for now
  AF_INET
  // AF_INET6 = 10
}

///|
fn AddrFamily::from_u16(value : UInt16) -> AddrFamily!Failure {
  match value {
    2 => AddrFamily::AF_INET
    // 10 => AddrFamily::AF_INET6
    _ => fail!("Unsupported address family: \{value}")
  }
}

///|
fn AddrFamily::to_u16(self : AddrFamily) -> UInt16 {
  match self {
    AddrFamily::AF_INET => 2
    // AddrFamily::AF_INET6 => 10
  }
}

///|
pub(all) enum SocketType {
  SOCK_STREAM
  SOCK_DGRAM
  // other types are not supported
}

///|
fn SocketType::from_u16(value : UInt16) -> SocketType!Failure {
  match value {
    1 => SocketType::SOCK_STREAM
    2 => SocketType::SOCK_DGRAM
    _ => fail!("Unsupported socket type: \{value}")
  }
}

///|
fn SocketType::to_u16(self : SocketType) -> UInt16 {
  match self {
    SocketType::SOCK_STREAM => 1
    SocketType::SOCK_DGRAM => 2
  }
}

///|
pub(all) enum IpProtocol {
  IPPROTO_IP
  IPPROTO_TCP
  IPPROTO_UDP
  // other protocols are not supported
}

///|
fn IpProtocol::from_u16(value : UInt16) -> IpProtocol!Failure {
  match value {
    0 => IpProtocol::IPPROTO_IP
    6 => IpProtocol::IPPROTO_TCP
    17 => IpProtocol::IPPROTO_UDP
    _ => fail!("Unsupported IP protocol: \{value}")
  }
}

///|
fn IpProtocol::to_u16(self : IpProtocol) -> UInt16 {
  match self {
    IpProtocol::IPPROTO_IP => 0
    IpProtocol::IPPROTO_TCP => 6
    IpProtocol::IPPROTO_UDP => 17
  }
}

// TODO: Protocol families

///|
struct Hostent {
  inner : @internal.FFI_hostent
}

///|
pub fn Hostent::get_addr_str(self : Hostent) -> String!Failure {
  let addr = @internal._ffi_hostent_get_addr_str(self.inner)
  @ctypes.CString::from_bytes!(addr).to_string!()
}

///|
pub fn get_host_by_name(hostname : String) -> Hostent!Failure {
  let cstr = @ctypes.CString::from_string(hostname)
  let hostent = @internal._ffi_gethostbyname(cstr)
  if hostent.is_null() {
    fail!("gethostbyname failed, hostent is null")
  }
  Hostent::{ inner: hostent }
}

///|
pub fn Hostent::get_addr(self : Hostent) -> UInt {
  @internal._ffi_hostent_get_addr(self.inner)
}

///|
struct Socket {
  domain : AddrFamily
  inner : Int
}

///|
pub fn new_socket(
  domain : AddrFamily,
  type_ : SocketType,
  protocol~ : IpProtocol? = None
) -> Socket!Failure {
  let protocol = if protocol.is_empty() {
    match type_ {
      SocketType::SOCK_STREAM => IpProtocol::IPPROTO_TCP
      SocketType::SOCK_DGRAM => IpProtocol::IPPROTO_UDP
    }
  } else {
    protocol.unwrap()
  }
  let fd = @internal._ffi_socket(
    domain.to_u16().to_int(),
    type_.to_u16().to_int(),
    protocol.to_u16().to_int(),
  )
  if fd < 0 {
    if fd == -1 {
      let err = @internal._ffi_get_errno()
      let err_str = @internal._ffi_stderror!(err)
      fail!("socket failed: \{err_str}")
    }
    fail!("socket failed: \{fd}")
  }
  Socket::{ domain, inner: fd }
}

///|
pub fn Socket::close(self : Socket) -> Unit!Failure {
  let result = @internal._ffi_close(self.inner)
  if result < 0 {
    let err_str = if result == -1 {
      let err = @internal._ffi_get_errno()
      let err = @internal._ffi_stderror!(err)
      "close failed: \{err}"
    } else {
      "close failed: \{result}"
    }
    fail!(err_str)
  }
}

///|
pub fn Socket::connect_addr(self : Socket, addr : SockaddrIn) -> Unit!Failure {
  let result = @internal._ffi_connect(self.inner, addr.inner)
  if result < 0 {
    let err_str = if result == -1 {
      let err = @internal._ffi_get_errno()
      let err = @internal._ffi_stderror!(err)
      "connect failed: \{err}"
    } else {
      "connect failed: \{result}"
    }
    self.close!()
    fail!(err_str)
  }
}

///|
pub fn Socket::connect(
  self : Socket,
  addr : Hostent,
  port : UInt16
) -> SockaddrIn!Failure {
  let sockaddr_in = new_sockaddr_in!()
  sockaddr_in
  ..set_family(self.domain)
  ..set_addr(addr.get_addr())
  ..set_port(port)
  self.connect_addr!(sockaddr_in)
  sockaddr_in
}

///|
pub fn Socket::bind_addr(self : Socket, addr : SockaddrIn) -> Unit!Failure {
  let result = @internal._ffi_bind(self.inner, addr.inner)
  if result < 0 {
    let err_str = if result == -1 {
      let err = @internal._ffi_get_errno()
      let err = @internal._ffi_stderror!(err)
      "bind failed: \{err}"
    } else {
      "bind failed: \{result}"
    }
    self.close!()
    fail!(err_str)
  }
}

///|
pub fn Socket::bind(
  self : Socket,
  addr : String,
  port : UInt16
) -> SockaddrIn!Failure {
  let sockaddr_in = new_sockaddr_in!()
  sockaddr_in..set_family(self.domain)..set_addr_str!(addr)..set_port(port)
  self.bind_addr!(sockaddr_in)
  sockaddr_in
}

///|
pub fn Socket::listen(self : Socket, backlog : Int) -> Unit!Failure {
  let result = @internal._ffi_listen(self.inner, backlog)
  if result < 0 {
    let err_str = if result == -1 {
      let err = @internal._ffi_get_errno()
      let err = @internal._ffi_stderror!(err)
      "listen failed: \{err}"
    } else {
      "listen failed: \{result}"
    }
    self.close!()
    fail!(err_str)
  }
}

///|
pub fn Socket::accept(self : Socket) -> (Socket, SockaddrIn)!Failure {
  let addr = new_sockaddr_in!()
  let fd = @internal._ffi_accept(self.inner, addr.inner)
  if fd < 0 {
    let err_str = if fd == -1 {
      let err = @internal._ffi_get_errno()
      let err = @internal._ffi_stderror!(err)
      "accept failed: \{err}"
    } else {
      "accept failed: \{fd}"
    }
    self.close!()
    fail!(err_str)
  }
  (Socket::{ domain: self.domain, inner: fd }, addr)
}

///|
trait Sendable {
  send(Self, socket : Socket, flags : Int) -> Int64!Failure
}

///|
pub impl Sendable for FixedArray[Byte] with send(
  self : FixedArray[Byte],
  socket : Socket,
  flags : Int
) -> Int64!Failure {
  @internal._ffi_send_byte_array(socket.inner, self, flags)
}

///|
pub impl Sendable for Bytes with send(
  self : Bytes,
  socket : Socket,
  flags : Int
) -> Int64!Failure {
  @internal._ffi_send_bytes(socket.inner, self, flags)
}

///|
pub fn Socket::send[Sendable : Sendable](
  self : Socket,
  sendable : Sendable,
  flags~ : Int = 0
) -> Int64!Failure {
  let result = sendable.send!(self, flags)
  if result < 0 {
    let err_str = if result == -1 {
      let err = @internal._ffi_get_errno()
      let err = @internal._ffi_stderror!(err)
      "send failed: \{err}"
    } else {
      "send failed: \{result}"
    }
    self.close!()
    fail!(err_str)
  }
  result
}

///|
trait SendToAble {
  send_to(Self, socket : Socket, flags : Int, addr : SockaddrIn) ->
       Int64!Failure
}

///|
pub impl SendToAble for FixedArray[Byte] with send_to(
  self : FixedArray[Byte],
  socket : Socket,
  flags : Int,
  addr : SockaddrIn
) -> Int64!Failure {
  @internal._ffi_sendto_byte_array(socket.inner, self, flags, addr.inner)
}

///|
pub impl SendToAble for Bytes with send_to(
  self : Bytes,
  socket : Socket,
  flags : Int,
  addr : SockaddrIn
) -> Int64!Failure {
  @internal._ffi_sendto_bytes(socket.inner, self, flags, addr.inner)
}

///|
pub fn Socket::send_to[SendToAble : SendToAble](
  self : Socket,
  sendable : SendToAble,
  addr : SockaddrIn,
  flags~ : Int = 0x800 // MSG_CONFIRM
) -> Int64!Failure {
  let result = sendable.send_to!(self, flags, addr)
  if result < 0 {
    let err_str = if result == -1 {
      let err = @internal._ffi_get_errno()
      let err = @internal._ffi_stderror!(err)
      "sendto failed: \{err}"
    } else {
      "sendto failed: \{result}"
    }
    self.close!()
    fail!(err_str)
  }
  result
}

///|
pub fn Socket::recv_buffer(
  self : Socket,
  buf : FixedArray[Byte],
  flags~ : Int = 0
) -> Int64!Failure {
  let result = @internal._ffi_recv(self.inner, buf, flags)
  if result < 0 {
    let err_str = if result == -1 {
      let err = @internal._ffi_get_errno()
      let err = @internal._ffi_stderror!(err)
      "recv failed: \{err}"
    } else {
      "recv failed: \{result}"
    }
    self.close!()
    fail!(err_str)
  }
  result
}

///|
pub fn Socket::recv(self : Socket, buf_size : Int) -> FixedArray[Byte]!Failure {
  guard buf_size > 0 else { fail!("recv: buf_size must be greater than 0") }
  let mut buf : FixedArray[Byte] = FixedArray::make(buf_size, (0).to_byte())
  let len = self.recv_buffer!(buf)
  if len.to_int() != buf_size {
    buf = FixedArray::makei(len.to_int(), fn(i) { buf[i] })
  }
  buf
}

///|
pub fn Socket::recv_from_buffer(
  self : Socket,
  buf : FixedArray[Byte],
  flags~ : Int = 0x100 // MSG_WAITALL
) -> (Int64, SockaddrIn)!Failure {
  let addr = new_sockaddr_in!()
  let result = @internal._ffi_recvfrom(self.inner, buf, flags, addr.inner)
  if result < 0 {
    let err_str = if result == -1 {
      let err = @internal._ffi_get_errno()
      let err = @internal._ffi_stderror!(err)
      "recvfrom failed: \{err}"
    } else {
      "recvfrom failed: \{result}"
    }
    self.close!()
    fail!(err_str)
  }
  (result, addr)
}

///|
pub fn Socket::recv_from(
  self : Socket,
  buf_size : Int
) -> (FixedArray[Byte], SockaddrIn)!Failure {
  guard buf_size > 0 else {
    fail!("recv_from: buf_size must be greater than 0")
  }
  let mut buf : FixedArray[Byte] = FixedArray::make(buf_size, 0)
  let (len, addr) = self.recv_from_buffer!(buf)
  if len.to_int() != buf_size {
    buf = FixedArray::makei(len.to_int(), fn(i) { buf[i] })
  }
  (buf, addr)
}

///|
struct SockaddrIn {
  inner : @internal.FFI_sockaddr_in
}

///|
pub fn new_sockaddr_in() -> SockaddrIn!Failure {
  let sockaddr = @internal._ffi_new_sockaddr_in()
  if sockaddr.is_null() {
    fail!("build_sockaddr_in failed, sockaddr is null")
  }
  SockaddrIn::{ inner: sockaddr }
}

///|
pub fn SockaddrIn::get_family(self : SockaddrIn) -> AddrFamily!Failure {
  AddrFamily::from_u16!(@internal._ffi_sockaddr_in_get_family(self.inner))
}

///|
pub fn SockaddrIn::set_family(self : SockaddrIn, family : AddrFamily) -> Unit {
  @internal._ffi_sockaddr_in_set_family(self.inner, family.to_u16())
}

///|
pub fn SockaddrIn::get_addr(self : SockaddrIn) -> UInt {
  @internal._ffi_sockaddr_in_get_addr(self.inner)
}

///|
pub fn SockaddrIn::set_addr(self : SockaddrIn, addr : UInt) -> Unit {
  @internal._ffi_sockaddr_in_set_addr(self.inner, addr)
}

///|
pub fn SockaddrIn::get_port(self : SockaddrIn) -> UInt16 {
  @internal._ffi_sockaddr_in_get_port(self.inner)
}

///|
pub fn SockaddrIn::set_port(self : SockaddrIn, port : UInt16) -> Unit {
  @internal._ffi_sockaddr_in_set_port(self.inner, port)
}

///|
pub fn SockaddrIn::get_addr_str(self : SockaddrIn) -> String!Failure {
  let addr = @internal._ffi_sockaddr_in_get_addr_str(self.inner)
  addr.to_string!()
}

///|
pub fn SockaddrIn::set_addr_str(
  self : SockaddrIn,
  addr : String
) -> Unit!Failure {
  let cstr = @ctypes.CString::from_string(addr)
  let retval = @internal._ffi_sockaddr_in_set_addr_str(self.inner, cstr)
  if retval != 0 {
    fail!("set_addr_str failed: \{retval}")
  }
}