
header "sys" as sys

use core
use coro
use io

class SocketTCP {
    ~ host: String
    ~ fd: FD
    ~ port: u16
    - addrinfo: AddrInfo
    - closed: bool (false)

    + static fn new(host: String, port: u16) SocketTCP !os_socket_create !invalid_host {
        // 
        #if OS == win
        core:WSA.init()
        #end

        // Get host info
        let addrinfo = AddrInfo.new(host, port) ! throw invalid_host

        #if OS == win
        let sock_fd = sys:socket(sys:AF_INET, sys:SOCK_STREAM, 0)
        let fd : FD = io:valk_fd(sock_fd.to(int))
        #else
        let ufd = sys:socket(sys:AF_INET, sys:SOCK_STREAM, 0)
        let fd : FD = ufd.@cast(FD)
        #end

        if fd == -1 : throw os_socket_create
        io:set_non_block(fd, true)

        // Set timeout
        #if OS == win
        let timeout : i32 = 1000 * 5 // 5 seconds
        sys:setsockopt(sock_fd, sys:SOL_SOCKET, sys:SO_RCVTIMEO, @ref(timeout), size_of(i32))
        #else
        let timeout = @stack(<sys:libc_timeval>)
        timeout.tv_sec = 5
        timeout.tv_usec = 0
        sys:setsockopt(ufd, sys:SOL_SOCKET, sys:SO_RCVTIMEO, timeout, size_of(<sys:libc_timeval>))
        #end

        // SO_NOSIGPIPE
        #if OS == macos
        let nosigv : i32 = 1
        sys:setsockopt(ufd, sys:SOL_SOCKET, sys:SO_NOSIGPIPE, @ref(nosigv), size_of(i32))
        #end

        return SocketTCP{
            host: host
            port: port
            fd: fd
            addrinfo: addrinfo
        }
    }

    + fn close() {
        if this.closed : return
        this.closed = true
        Socket.close(this.fd)
    }

    fn gc_free() {
        this.close()
    }

    + fn connect() Connection !closed !connection_failed {

        if this.closed : throw closed

        let fd = this.fd
        let ofd = io:os_fd(this.fd)

        while true {
            let err = sys:connect(ofd, this.addrinfo.sock_addr(), this.addrinfo.addr_len())
            if err != 0 {
                let e = core:socket_errno()
                #if OS == win
                if e == sys:WSAEISCONN : break
                let a = e == sys:WSAEWOULDBLOCK || e == sys:WSAEINPROGRESS || e == sys:WSAEALREADY
                #else
                if e == sys:EISCONN : break
                let a = e == sys:EINPROGRESS
                #end
                if a {
                    coro:await_fd(fd, true, true)
                    continue
                }
                throw connection_failed
            }
        }

        this.closed = true // The connection will close the FD
        return Connection.new(this.fd)
    }

    + fn bind() !closed !bind !listen {

        if this.closed : throw closed

        let yes : i32 = 1
        let yes_ptr = @ref(yes)
        
        let err : i32 = 0

        sys:setsockopt(io:os_fd(this.fd), sys:SOL_SOCKET, sys:SO_REUSEADDR, yes_ptr, size_of(i32))

        err = sys:bind(io:os_fd(this.fd), this.addrinfo.sock_addr(), this.addrinfo.addr_len())
        if err != 0 : throw bind

        err = sys:listen(io:os_fd(this.fd), 2000000)
        if err != 0 : throw listen
    }

    + fn accept() Connection !too_many_connections !error !closed {

        if this.closed : throw closed

        while true {
            let ofd = sys:accept(io:os_fd(this.fd), null, null)
            let fd = io:valk_fd(ofd.to(int))

            // Error
            if fd == -1 {
                #if OS == win
                // Windows error
                let err = core:WSA.error()
                if err == sys:WSAEWOULDBLOCK {
                    coro:await_fd(this.fd, true, false)
                    continue
                }
                #else
                // Linux / macos error
                let err = core:libc_errno()
                if err == sys:EAGAIN {
                    coro:await_fd(this.fd, true, false)
                    continue
                }
                #end
                // On error
                if err == sys:EMFILE || err == sys:ENFILE {
                    throw too_many_connections
                }
                throw error
            }

            return Connection.new(fd)
        }
        throw error
    }

    // fn clone() SELF {
    //     return SELF {
    //         host: this.host
    //         fd: this.fd
    //         port: this.port
    //         addrinfo: SELF.get_addrinfo(this.host, this.port) !? this.addrinfo.@cast(sys:libc_addrinfo_fix)
    //     }
    // }
}
