import io.errors.*;
import io.sockets.platform;
import byteorder.*;
public import io.sockets.platform.(
    Sockaddr, Sockaddr_in, Sockaddr_storage,
    In_addr,
    AF_UNSPEC, AF_INET, AF_INET6,
    PF_UNSPEC, PF_INET, PF_INET6,
    INADDR_ANY, INADDR_BROADCAST, INADDR_LOOPBACK, INADDR_NONE,
    INADDR_UNSPEC_GROUP, INADDR_ALLHOSTS_GROUP, INADDR_ALLRTRS_GROUP,
    INADDR_MAX_LOCAL_GROUP,
    SOCK_STREAM, SOCK_DGRAM, SOCK_RAW,
    IPPROTO_ICMP, IPPROTO_IGMP, IPPROTO_TCP, IPPROTO_UDP, IPPROTO_ICMPV6,
    startSockets, finishSockets, SOCKET_ERROR,
    SocklenT,
    SOL_SOCKET, SO_REUSEADDR
);
import libc.(memset);

private alias platform = io.sockets.platform;



//
// wrapper for socket API calls
//

record GenericSocketError (code: Int);

variant SocketError (GenericSocketError);

instance IOError (SocketError);

[T | VariantMember?(SocketError, T)]
overload Exception(forward x:T) = Exception(SocketError(x));

overload printTo(stream, x:GenericSocketError) {
    printTo(stream, "error(", x.code, "): ");
    printErrorMessageTo(stream, x.code);
}

overload printReprTo(stream, x:GenericSocketError) {
    printTo(stream, "GenericSocketError(", errorCodeName(x.code), ")");
}

socketCall(callable, forward ..args) {
    var ret = callable(..args);
    if (ret == wrapCast(Type(ret), platform.SOCKET_ERROR))
        throw GenericSocketError(platform.errno());
    return ret;
}



//
// high-level socket addresses
//

// makeSockaddr(addr) --> Sockaddr
define makeSockaddr;
// sockaddrDomain(addr) --> PF_*
define sockaddrDomain;
// sockaddrStreamProtocol(addr) --> *PROTO_*
define sockaddrStreamProtocol;
// sockaddrDgramProtocol(addr) --> *PROTO_*
define sockaddrDgramProtocol;

SocketAddress?(T)
    = CallDefined?(makeSockaddr, T)
      and CallDefined?(sockaddrDomain, T);
StreamSocketAddress?(T)
    = SocketAddress?(T)
      and CallDefined?(sockaddrStreamProtocol, T)
      and CallDefined?(T, Sockaddr_storage, SocklenT);
DgramSocketAddress?(T)
    = SocketAddress?(T)
      and CallDefined?(sockaddrDgramProtocol, T);

[T | SocketAddress?(T)]
makeSockaddrParams(addr: T)
    --> sockaddr: Type(makeSockaddr(typeToRValue(T))),
        psockaddr: Pointer[Sockaddr],
        szsockaddr: SocklenT
{
    sockaddr <-- makeSockaddr(addr);
    psockaddr <-- Pointer[Sockaddr](&sockaddr);
    szsockaddr <-- SocklenT(TypeSize(Type(sockaddr)));
}

// lookupInetHostname(host) --> In_addr
define lookupInetHostname;
InetHostname?(T) = CallDefined?(lookupInetHostname, T);
record Inet (host: In_addr, port: UInt16);

[T, I | InetHostname?(T) and T != In_addr and Integer?(I)]
overload Inet(host: T, port: I)
    = Inet(lookupInetHostname(host), UInt16(port));

record InvalidInetAddress(sockaddr: Sockaddr_storage, szsockaddr: SocklenT);
instance SocketError (InvalidInetAddress);

overload Inet(sockaddr: Sockaddr_storage, szsockaddr: SocklenT)
    --> addr: Inet
{
    if (sockaddr.ss_family != AF_INET
        or szsockaddr < TypeSize(Sockaddr_in))
        throw InvalidInetAddress(sockaddr, szsockaddr);
    var psockaddr_in = Pointer[Sockaddr_in](&sockaddr);
    addr.host <-- psockaddr_in^.sin_addr;
    addr.port <-- networkToHost(psockaddr_in^.sin_port);
}

overload makeSockaddr(inet: Inet) --> sin: Sockaddr_in {
    memset(RawPointer(&sin), 0, TypeSize(Sockaddr_in));
    sin.sin_family = AF_INET;
    sin.sin_port <-- hostToNetwork(inet.port);
    sin.sin_addr <-- inet.host;
}

overload sockaddrDomain(inet: Inet) = PF_INET;
overload sockaddrStreamProtocol(inet: Inet) = IPPROTO_TCP;
overload sockaddrDgramProtocol(inet: Inet) = IPPROTO_UDP;

overload lookupInetHostname(n: UInt32) --> s: In_addr {
    platform.s_addr(s) <-- hostToNetwork(n);
}

overload lookupInetHostname(n: In_addr) = n;

record HostnameLookupError(hostname: String);
instance SocketError (HostnameLookupError);
[S | String?(S) and S != String]
overload HostnameLookupError(name: S) = HostnameLookupError(String(name));

[S | CCompatibleString?(S)]
overload lookupInetHostname(hostname: S) {
    var hostent = platform.gethostbyname(cstring(hostname));
    if (null?(hostent))
        throw HostnameLookupError(hostname);
    if (hostent^.h_addrtype != AF_INET
        or hostent^.h_length < TypeSize(In_addr)
        or null?(hostent^.h_addr_list[0]))
        throw HostnameLookupError(hostname);

    return Pointer[In_addr](hostent^.h_addr_list[0])^;
}



//
// socket objects
//
Socket?(X) = false;

record ListenSocket[Addr] (raw: platform.RawSocket);
record StreamSocket (raw: platform.RawSocket);

[Addr] overload Socket?(static ListenSocket[Addr]) = true;
overload Socket?(static StreamSocket) = true;

[S | Socket?(S)] overload RegularRecord?(static S) = false;

[S | Socket?(S)]
overload destroy(s: S) { if (s.raw != SOCKET_ERROR) platform.close(s.raw); }

[S | Socket?(S)]
overload BitwiseMovedType?(static S) = true;

[S | Socket?(S)]
overload resetUnsafe(s: S) { s.raw = SOCKET_ERROR; }

[S | Socket?(S)]
overload S(x: S);

[S | Socket?(S)]
overload assign(ref x: S, ref y: S);

[S | Socket?(S)]
overload fileHandle(s: S) { return s.raw; }



//
// ListenSocket()
//
[T, I | StreamSocketAddress?(T) and Integer?(I)]
overload ListenSocket(addr: T, backlog: I)
    --> socket: ListenSocket[T]
{
    socket.raw <-- socketCall(platform.socket,
        Int(sockaddrDomain(addr)),
        SOCK_STREAM,
        Int(sockaddrStreamProtocol(addr))
    );
    try {
        var sockaddr, psockaddr, szsockaddr = ..makeSockaddrParams(addr);
        var sockoptValue = 1;
        socketCall(platform.setsockopt, socket.raw,
            SOL_SOCKET, SO_REUSEADDR, platform.DataPointer(&sockoptValue), SocklenT(TypeSize(Int))
        );
        socketCall(platform.bind, socket.raw, psockaddr, szsockaddr);
        socketCall(platform.listen, socket.raw, Int(backlog));
    } catch (ex) {
        platform.close(socket.raw);
        throw ex;
    }
}

[T | StreamSocketAddress?(T)]
overload ListenSocket(addr: T) = ListenSocket(addr, 1);

[T]
accept(listener: ListenSocket[T])
    --> socket: StreamSocket,
        addr: T
{
    var sockaddr = Sockaddr_storage();
    var psockaddr = Pointer[Sockaddr](&sockaddr);
    var szsockaddr = SocklenT(TypeSize(Sockaddr_storage));

    socket.raw <-- socketCall(platform.accept, listener.raw, psockaddr, &szsockaddr);
    try {
        addr <-- T(sockaddr, szsockaddr);
    } catch (ex) {
        platform.close(socket.raw);
        throw ex;
    }
}



//
// StreamSocket()
//
[T]
overload StreamSocket(listener: ListenSocket[T])
    = takeValues(static 1, ..accept(listener));

[T | StreamSocketAddress?(T)]
overload StreamSocket(addr: T) --> socket: StreamSocket {
    socket.raw <-- socketCall(platform.socket,
        Int(sockaddrDomain(addr)),
        SOCK_STREAM,
        Int(sockaddrStreamProtocol(addr))
    );
    
    try {
        var sockaddr, psockaddr, szsockaddr = ..makeSockaddrParams(addr);
        socketCall(platform.connect, socket.raw, psockaddr, szsockaddr);
    } catch (ex) {
        platform.close(socket.raw);
        throw ex;
    }
}



//
// StreamSocket implementation of stream protocol
//
overload read(s: StreamSocket, buffer: Pointer[Byte], n: SizeT)
    = SizeT(socketCall(platform.recv, s.raw,
        platform.DataPointer(buffer), platform.DataSize(n),
        0));

overload write(s: StreamSocket, buffer: Pointer[Byte], n: SizeT) {
    socketCall(platform.send, s.raw,
        platform.DataPointer(buffer), platform.DataSize(n),
        0);
}

overload flush(s: StreamSocket) { }



//
// Print support for In_addr
//
overload printTo(stream, addr: In_addr) {
    var host = networkToHost(platform.s_addr(addr));
    printTo(stream,
               bitshr(host, 24),         ".",
        bitand(bitshr(host, 16), 0xFFu), ".",
        bitand(bitshr(host,  8), 0xFFu), ".",
        bitand(       host,      0xFFu),
    );
}
