#+build freebsd
package net

/*
	Package net implements cross-platform Berkeley Sockets, DNS resolution and associated procedures.
	For other protocols and their features, see subdirectories of this package.
*/

/*
	Copyright 2022 Tetralux        <tetraluxonpc@gmail.com>
	Copyright 2022 Colin Davidson  <colrdavidson@gmail.com>
	Copyright 2022 Jeroen van Rijn <nom@duclavier.com>.
	Copyright 2024 Feoramund       <rune@swevencraft.org>.
	Made available under Odin's license.

	List of contributors:
		Tetralux:        Initial implementation
		Colin Davidson:  Linux platform code, OSX platform code, Odin-native DNS resolver
		Jeroen van Rijn: Cross platform unification, code style, documentation
		Feoramund:       FreeBSD platform code
*/

import "core:sys/freebsd"
import "core:time"

Fd :: freebsd.Fd

_SOCKET_OPTION_BROADCAST                 :: freebsd.Socket_Option.BROADCAST
_SOCKET_OPTION_REUSE_ADDRESS             :: freebsd.Socket_Option.REUSEADDR
_SOCKET_OPTION_KEEP_ALIVE                :: freebsd.Socket_Option.KEEPALIVE
_SOCKET_OPTION_OUT_OF_BOUNDS_DATA_INLINE :: freebsd.Socket_Option.OOBINLINE
_SOCKET_OPTION_LINGER                    :: freebsd.Socket_Option.LINGER
_SOCKET_OPTION_RECEIVE_BUFFER_SIZE       :: freebsd.Socket_Option.RCVBUF
_SOCKET_OPTION_SEND_BUFFER_SIZE          :: freebsd.Socket_Option.SNDBUF
_SOCKET_OPTION_RECEIVE_TIMEOUT           :: freebsd.Socket_Option.RCVTIMEO
_SOCKET_OPTION_SEND_TIMEOUT              :: freebsd.Socket_Option.SNDTIMEO

_SOCKET_OPTION_TCP_NODELAY :: -1

_SOCKET_OPTION_USE_LOOPBACK              :: freebsd.Socket_Option.USELOOPBACK
_SOCKET_OPTION_REUSE_PORT                :: freebsd.Socket_Option.REUSEPORT
_SOCKET_OPTION_NO_SIGPIPE_FROM_EPIPE     :: freebsd.Socket_Option.NOSIGPIPE
_SOCKET_OPTION_REUSE_PORT_LOAD_BALANCING :: freebsd.Socket_Option.REUSEPORT_LB

_SOCKET_OPTION_EXCLUSIVE_ADDR_USE :: -1
_SOCKET_OPTION_CONDITIONAL_ACCEPT :: -1
_SOCKET_OPTION_DONT_LINGER        :: -1

_SHUTDOWN_MANNER_RECEIVE :: freebsd.Shutdown_Method.RD
_SHUTDOWN_MANNER_SEND    :: freebsd.Shutdown_Method.WR
_SHUTDOWN_MANNER_BOTH    :: freebsd.Shutdown_Method.RDWR

@(private)
_create_socket :: proc(family: Address_Family, protocol: Socket_Protocol) -> (socket: Any_Socket, err: Create_Socket_Error) {
	sys_family:      freebsd.Protocol_Family = ---
	sys_protocol:    freebsd.Protocol        = ---
	sys_socket_type: freebsd.Socket_Type     = ---

	switch family {
	case .IP4: sys_family = .INET
	case .IP6: sys_family = .INET6
	}

	switch protocol {
	case .TCP: sys_protocol = .TCP; sys_socket_type = .STREAM
	case .UDP: sys_protocol = .UDP; sys_socket_type = .DGRAM
	}

	new_socket, errno := freebsd.socket(sys_family, sys_socket_type, sys_protocol)
	if errno != nil {
		err = _create_socket_error(errno)
		return
	}

	switch protocol {
	case .TCP: return cast(TCP_Socket)new_socket, nil
	case .UDP: return cast(UDP_Socket)new_socket, nil
	}

	return
}

@(private)
_dial_tcp_from_endpoint :: proc(endpoint: Endpoint, options := DEFAULT_TCP_OPTIONS) -> (socket: TCP_Socket, err: Network_Error) {
	if endpoint.port == 0 {
		return 0, .Port_Required
	}

	family := family_from_endpoint(endpoint)
	new_socket := create_socket(family, .TCP) or_return
	socket = new_socket.(TCP_Socket)

	sockaddr := _endpoint_to_sockaddr(endpoint)
	errno := freebsd.connect(cast(Fd)socket, &sockaddr, cast(freebsd.socklen_t)sockaddr.len)
	if errno != nil {
		close(socket)
		return {}, _dial_error(errno)
	}

	return
}

@(private)
_bind :: proc(socket: Any_Socket, ep: Endpoint) -> (err: Bind_Error) {
	sockaddr := _endpoint_to_sockaddr(ep)
	real_socket := any_socket_to_socket(socket)
	errno := freebsd.bind(cast(Fd)real_socket, &sockaddr, cast(freebsd.socklen_t)sockaddr.len)
	if errno != nil {
		err = _bind_error(errno)
	}
	return
}

@(private)
_listen_tcp :: proc(interface_endpoint: Endpoint, backlog := 1000) -> (socket: TCP_Socket, err: Network_Error) {
	family := family_from_endpoint(interface_endpoint)
	new_socket := create_socket(family, .TCP) or_return
	socket = new_socket.(TCP_Socket)
	defer if err != nil { close(socket) }

	bind(socket, interface_endpoint) or_return

	errno := freebsd.listen(cast(Fd)socket, backlog)
	if errno != nil {
		err = _listen_error(errno)
		return
	}

	return
}

@(private)
_bound_endpoint :: proc(sock: Any_Socket) -> (ep: Endpoint, err: Socket_Info_Error) {
	sockaddr: freebsd.Socket_Address_Storage

	errno := freebsd.getsockname(cast(Fd)any_socket_to_socket(sock), &sockaddr)
	if errno != nil {
		err = _socket_info_error(errno)
		return
	}

	ep = _sockaddr_to_endpoint(&sockaddr)
	return
}

@(private)
_peer_endpoint :: proc(sock: Any_Socket) -> (ep: Endpoint, err: Socket_Info_Error) {
	sockaddr: freebsd.Socket_Address_Storage

	errno := freebsd.getpeername(cast(Fd)any_socket_to_socket(sock), &sockaddr)
	if errno != nil {
		err = _socket_info_error(errno)
		return
	}

	ep = _sockaddr_to_endpoint(&sockaddr)
	return
}

@(private)
_accept_tcp :: proc(sock: TCP_Socket, options := DEFAULT_TCP_OPTIONS) -> (client: TCP_Socket, source: Endpoint, err: Accept_Error) {
	sockaddr: freebsd.Socket_Address_Storage

	result, errno := freebsd.accept(cast(Fd)sock, &sockaddr)
	if errno != nil {
		err = _accept_error(errno)
		return
	}

	client = cast(TCP_Socket)result
	source = _sockaddr_to_endpoint(&sockaddr)
	return
}

@(private)
_close :: proc(socket: Any_Socket) {
	real_socket := cast(Fd)any_socket_to_socket(socket)
	// TODO: This returns an error number, but the `core:net` interface does not handle it.
	_ = freebsd.close(real_socket)
}

@(private)
_recv_tcp :: proc(socket: TCP_Socket, buf: []byte) -> (bytes_read: int, err: TCP_Recv_Error) {
	if len(buf) == 0 {
		return
	}
	result, errno := freebsd.recv(cast(Fd)socket, buf, .NONE)
	if errno != nil {
		err = _tcp_recv_error(errno)
		return
	}
	return result, nil
}

@(private)
_recv_udp :: proc(socket: UDP_Socket, buf: []byte) -> (bytes_read: int, remote_endpoint: Endpoint, err: UDP_Recv_Error) {
	if len(buf) == 0 {
		return
	}
	from: freebsd.Socket_Address_Storage

	result, errno := freebsd.recvfrom(cast(Fd)socket, buf, .NONE, &from)
	if errno != nil {
		err = _udp_recv_error(errno)
		return
	}
	return result, _sockaddr_to_endpoint(&from), nil
}

@(private)
_send_tcp :: proc(socket: TCP_Socket, buf: []byte) -> (bytes_written: int, err: TCP_Send_Error) {
	for bytes_written < len(buf) {
		limit := min(int(max(i32)), len(buf) - bytes_written)
		remaining := buf[bytes_written:][:limit]

		result, errno := freebsd.send(cast(Fd)socket, remaining, .NOSIGNAL)
		if errno != nil {
			err = _tcp_send_error(errno)
			return
		}
		bytes_written += result
	}
	return
}

@(private)
_send_udp :: proc(socket: UDP_Socket, buf: []byte, to: Endpoint) -> (bytes_written: int, err: UDP_Send_Error) {
	toaddr := _endpoint_to_sockaddr(to)
	for bytes_written < len(buf) {
		limit := min(int(max(i32)), len(buf) - bytes_written)
		remaining := buf[bytes_written:][:limit]

		result, errno := freebsd.sendto(cast(Fd)socket, remaining, .NOSIGNAL, &toaddr)
		if errno != nil {
			err = _udp_send_error(errno)
			return
		}
		bytes_written += result
	}
	return
}

@(private)
_shutdown :: proc(socket: Any_Socket, manner: Shutdown_Manner) -> (err: Shutdown_Error) {
	real_socket := cast(Fd)any_socket_to_socket(socket)
	errno := freebsd.shutdown(real_socket, cast(freebsd.Shutdown_Method)manner)
	if errno != nil {
		return _shutdown_error(errno)
	}
	return
}

@(private)
_set_option :: proc(socket: Any_Socket, option: Socket_Option, value: any, loc := #caller_location) -> Socket_Option_Error {
	// NOTE(Feoramund): I found that FreeBSD, like Linux, requires at least 32
	// bits for a boolean socket option value. Nothing less will work.
	bool_value: b32
	// TODO: Assuming no larger than i32, but the system may accept i64.
	int_value: i32
	timeval_value: freebsd.timeval

	ptr: rawptr
	len: freebsd.socklen_t

	#partial switch option {
	case
		.Reuse_Address,
		.Keep_Alive,
		.Broadcast,
		.Use_Loopback,
		.Out_Of_Bounds_Data_Inline,
		.Reuse_Port,
		.No_SIGPIPE_From_EPIPE,
		.Reuse_Port_Load_Balancing:
			switch real in value {
			case bool: bool_value = cast(b32)real
			case b8:   bool_value = cast(b32)real
			case b16:  bool_value = cast(b32)real
			case b32:  bool_value = real
			case b64:  bool_value = cast(b32)real
			case:
				panic("set_option() value must be a boolean here", loc)
			}
			ptr = &bool_value
			len = size_of(bool_value)
	case
		.Linger,
		.Send_Timeout,
		.Receive_Timeout:
			t, ok := value.(time.Duration)
			if !ok {
				panic("set_option() value must be a time.Duration here", loc)
			}

			micros := cast(freebsd.time_t)time.duration_microseconds(t)
			timeval_value.usec = cast(freebsd.suseconds_t)micros % 1e6
			timeval_value.sec = (micros - cast(freebsd.time_t)timeval_value.usec) / 1e6

			ptr = &timeval_value
			len = size_of(timeval_value)
	case
		.Receive_Buffer_Size,
		.Send_Buffer_Size:
			switch real in value {
			case   i8: int_value = cast(i32)real
			case   u8: int_value = cast(i32)real
			case  i16: int_value = cast(i32)real
			case  u16: int_value = cast(i32)real
			case  i32: int_value = real
			case  u32:
				if real > u32(max(i32)) { return .Invalid_Value }
				int_value = cast(i32)real
			case  i64:
				if real > i64(max(i32)) || real < i64(min(i32)) { return .Invalid_Value }
				int_value = cast(i32)real
			case  u64:
				if real > u64(max(i32)) { return .Invalid_Value }
				int_value = cast(i32)real
			case i128:
				if real > i128(max(i32)) || real < i128(min(i32)) { return .Invalid_Value }
				int_value = cast(i32)real
			case u128:
				if real > u128(max(i32)) { return .Invalid_Value }
				int_value = cast(i32)real
			case  int:
				if real > int(max(i32)) || real < int(min(i32)) { return .Invalid_Value }
				int_value = cast(i32)real
			case uint:
				if real > uint(max(i32)) { return .Invalid_Value }
				int_value = cast(i32)real
			case:
				panic("set_option() value must be an integer here", loc)
			}
			ptr = &int_value
			len = size_of(int_value)
	case:
		return .Invalid_Option
	}

	real_socket := any_socket_to_socket(socket)
	errno := freebsd.setsockopt(cast(Fd)real_socket, .SOCKET, cast(freebsd.Socket_Option)option, ptr, len)
	if errno != nil {
		return _socket_option_error(errno)
	}

	return nil
}

@(private)
_set_blocking :: proc(socket: Any_Socket, should_block: bool) -> (err: Set_Blocking_Error) {
	real_socket := any_socket_to_socket(socket)

	flags, errno := freebsd.fcntl_getfl(cast(freebsd.Fd)real_socket)
	if errno != nil {
		return _set_blocking_error(errno)
	}

	if should_block {
		flags &= ~{ .NONBLOCK }
	} else {
		flags |= { .NONBLOCK }
	}

	errno = freebsd.fcntl_setfl(cast(freebsd.Fd)real_socket, flags)
	if errno != nil {
		return _set_blocking_error(errno)
	}

	return
}

@(private)
_endpoint_to_sockaddr :: proc(ep: Endpoint) -> (sockaddr: freebsd.Socket_Address_Storage) {
	switch addr in ep.address {
	case IP4_Address:
		(cast(^freebsd.Socket_Address_Internet)(&sockaddr))^ = {
			len = size_of(freebsd.Socket_Address_Internet),
			family = .INET,
			port = cast(freebsd.in_port_t)ep.port,
			addr = transmute(freebsd.IP4_Address)addr,
		}
	case IP6_Address:
		(cast(^freebsd.Socket_Address_Internet6)(&sockaddr))^ = {
			len = size_of(freebsd.Socket_Address_Internet6),
			family = .INET6,
			port = cast(freebsd.in_port_t)ep.port,
			addr = transmute(freebsd.IP6_Address)addr,
		}
	}
	return
}

@(private)
_sockaddr_to_endpoint :: proc(native_addr: ^freebsd.Socket_Address_Storage) -> (ep: Endpoint) {
	#partial switch native_addr.family {
	case .INET:
		addr := cast(^freebsd.Socket_Address_Internet)native_addr
		ep = {
			address = cast(IP4_Address)addr.addr.addr8,
			port = cast(int)addr.port,
		}
	case .INET6:
		addr := cast(^freebsd.Socket_Address_Internet6)native_addr
		ep = {
			address = cast(IP6_Address)addr.addr.addr16,
			port = cast(int)addr.port,
		}
	case:
		panic("native_addr is neither an IP4 or IP6 address")
	}
	return
}
