#
#
#            Nim's Runtime Library
#        (c) Copyright 2013 Andreas Rumpf, Dominik Picheta
#
#    See the file "copying.txt", included in this
#    distribution, for details about the copyright.
#

## **Warning:** Since version 0.10.2 this module is deprecated.
## Use the `net <net.html>`_ or the
## `rawsockets <rawsockets.html>`_ module instead.
##
## This module implements portable sockets, it supports a mix of different types
## of sockets. Sockets are buffered by default meaning that data will be
## received in ``BufferSize`` (4000) sized chunks, buffering
## behaviour can be disabled by setting the ``buffered`` parameter when calling
## the ``socket`` function to `false`. Be aware that some functions may not yet
## support buffered sockets (mainly the recvFrom function).
##
## Most procedures raise OSError on error, but some may return ``-1`` or a
## boolean ``false``.
##
## SSL is supported through the OpenSSL library. This support can be activated
## by compiling with the ``-d:ssl`` switch. When an SSL socket is used it will
## raise ESSL exceptions when SSL errors occur.
##
## Asynchronous sockets are supported, however a better alternative is to use
## the `asyncio <asyncio.html>`_ module.

{.deprecated.}

include "system/inclrtl"

{.deadCodeElim: on.}

when hostOS == "solaris":
  {.passl: "-lsocket -lnsl".}

import os, parseutils
from times import epochTime
import unsigned

when defined(ssl):
  import openssl

when defined(Windows):
  import winlean
else:
  import posix

# Note: The enumerations are mapped to Window's constants.

when defined(ssl):  

  type
    SSLError* = object of Exception

    SSLCVerifyMode* = enum
      CVerifyNone, CVerifyPeer
    
    SSLProtVersion* = enum
      protSSLv2, protSSLv3, protTLSv1, protSSLv23
    
    SSLContext* = distinct SSLCTX

    SSLAcceptResult* = enum
      AcceptNoClient = 0, AcceptNoHandshake, AcceptSuccess

  {.deprecated: [ESSL: SSLError, TSSLCVerifyMode: SSLCVerifyMode,
     TSSLProtVersion: SSLProtVersion, PSSLContext: SSLContext,
     TSSLAcceptResult: SSLAcceptResult].}

const
  BufferSize*: int = 4000 ## size of a buffered socket's buffer

type
  TSocketImpl = object ## socket type
    fd: SocketHandle
    case isBuffered: bool # determines whether this socket is buffered.
    of true:
      buffer: array[0..BufferSize, char]
      currPos: int # current index in buffer
      bufLen: int # current length of buffer
    of false: nil
    when defined(ssl):
      case isSsl: bool
      of true:
        sslHandle: SSLPtr
        sslContext: SSLContext
        sslNoHandshake: bool # True if needs handshake.
        sslHasPeekChar: bool
        sslPeekChar: char
      of false: nil
    nonblocking: bool
  
  Socket* = ref TSocketImpl
  
  Port* = distinct uint16  ## port type
  
  Domain* = enum    ## domain, which specifies the protocol family of the
                    ## created socket. Other domains than those that are listed
                    ## here are unsupported.
    AF_UNIX,        ## for local socket (using a file). Unsupported on Windows.
    AF_INET = 2,    ## for network protocol IPv4 or
    AF_INET6 = 23   ## for network protocol IPv6.

  SockType* = enum     ## second argument to `socket` proc
    SOCK_STREAM = 1,   ## reliable stream-oriented service or Stream Sockets
    SOCK_DGRAM = 2,    ## datagram service or Datagram Sockets
    SOCK_RAW = 3,      ## raw protocols atop the network layer.
    SOCK_SEQPACKET = 5 ## reliable sequenced packet service

  Protocol* = enum      ## third argument to `socket` proc
    IPPROTO_TCP = 6,    ## Transmission control protocol. 
    IPPROTO_UDP = 17,   ## User datagram protocol.
    IPPROTO_IP,         ## Internet protocol. Unsupported on Windows.
    IPPROTO_IPV6,       ## Internet Protocol Version 6. Unsupported on Windows.
    IPPROTO_RAW,        ## Raw IP Packets Protocol. Unsupported on Windows.
    IPPROTO_ICMP        ## Control message protocol. Unsupported on Windows.

  Servent* = object ## information about a service
    name*: string
    aliases*: seq[string]
    port*: Port
    proto*: string

  Hostent* = object ## information about a given host
    name*: string
    aliases*: seq[string]
    addrtype*: Domain
    length*: int
    addrList*: seq[string]

  SOBool* = enum ## Boolean socket options.
    OptAcceptConn, OptBroadcast, OptDebug, OptDontRoute, OptKeepAlive,
    OptOOBInline, OptReuseAddr

  RecvLineResult* = enum ## result for recvLineAsync
    RecvFullLine, RecvPartialLine, RecvDisconnected, RecvFail

  ReadLineResult* = enum ## result for readLineAsync
    ReadFullLine, ReadPartialLine, ReadDisconnected, ReadNone

  TimeoutError* = object of Exception

{.deprecated: [TSocket: Socket, TType: SockType, TPort: Port, TDomain: Domain,
    TProtocol: Protocol, TServent: Servent, THostent: Hostent,
    TSOBool: SOBool, TRecvLineResult: RecvLineResult, 
    TReadLineResult: ReadLineResult, ETimeout: TimeoutError].}

when defined(booting):
  let invalidSocket*: Socket = nil ## invalid socket
else:
  const invalidSocket*: Socket = nil ## invalid socket

when defined(windows):
  let
    osInvalidSocket = winlean.INVALID_SOCKET
else:
  let
    osInvalidSocket = posix.INVALID_SOCKET

proc newTSocket(fd: SocketHandle, isBuff: bool): Socket =
  if fd == osInvalidSocket:
    return nil
  new(result)
  result.fd = fd
  result.isBuffered = isBuff
  if isBuff:
    result.currPos = 0
  result.nonblocking = false

proc `==`*(a, b: Port): bool {.borrow.}
  ## ``==`` for ports.

proc `$`*(p: Port): string {.borrow.}
  ## returns the port number as a string

proc ntohl*(x: int32): int32 = 
  ## Converts 32-bit integers from network to host byte order.
  ## On machines where the host byte order is the same as network byte order,
  ## this is a no-op; otherwise, it performs a 4-byte swap operation.
  when cpuEndian == bigEndian: result = x
  else: result = (x shr 24'i32) or
                 (x shr 8'i32 and 0xff00'i32) or
                 (x shl 8'i32 and 0xff0000'i32) or
                 (x shl 24'i32)

proc ntohs*(x: int16): int16 =
  ## Converts 16-bit integers from network to host byte order. On machines
  ## where the host byte order is the same as network byte order, this is
  ## a no-op; otherwise, it performs a 2-byte swap operation.
  when cpuEndian == bigEndian: result = x
  else: result = (x shr 8'i16) or (x shl 8'i16)

proc htonl*(x: int32): int32 =
  ## Converts 32-bit integers from host to network byte order. On machines
  ## where the host byte order is the same as network byte order, this is
  ## a no-op; otherwise, it performs a 4-byte swap operation.
  result = sockets.ntohl(x)

proc htons*(x: int16): int16 =
  ## Converts 16-bit positive integers from host to network byte order.
  ## On machines where the host byte order is the same as network byte
  ## order, this is a no-op; otherwise, it performs a 2-byte swap operation.
  result = sockets.ntohs(x)
  
when defined(Posix):
  proc toInt(domain: Domain): cint =
    case domain
    of AF_UNIX:        result = posix.AF_UNIX
    of AF_INET:        result = posix.AF_INET
    of AF_INET6:       result = posix.AF_INET6
    else: discard

  proc toInt(typ: SockType): cint =
    case typ
    of SOCK_STREAM:    result = posix.SOCK_STREAM
    of SOCK_DGRAM:     result = posix.SOCK_DGRAM
    of SOCK_SEQPACKET: result = posix.SOCK_SEQPACKET
    of SOCK_RAW:       result = posix.SOCK_RAW
    else: discard

  proc toInt(p: Protocol): cint =
    case p
    of IPPROTO_TCP:    result = posix.IPPROTO_TCP
    of IPPROTO_UDP:    result = posix.IPPROTO_UDP
    of IPPROTO_IP:     result = posix.IPPROTO_IP
    of IPPROTO_IPV6:   result = posix.IPPROTO_IPV6
    of IPPROTO_RAW:    result = posix.IPPROTO_RAW
    of IPPROTO_ICMP:   result = posix.IPPROTO_ICMP
    else: discard

else:
  proc toInt(domain: Domain): cint = 
    result = toU16(ord(domain))

  proc toInt(typ: SockType): cint =
    result = cint(ord(typ))
  
  proc toInt(p: Protocol): cint =
    result = cint(ord(p))

proc socket*(domain: Domain = AF_INET, typ: SockType = SOCK_STREAM,
             protocol: Protocol = IPPROTO_TCP, buffered = true): Socket =
  ## Creates a new socket; returns `InvalidSocket` if an error occurs.
  
  # TODO: Perhaps this should just raise EOS when an error occurs.
  when defined(Windows):
    result = newTSocket(winlean.socket(ord(domain), ord(typ), ord(protocol)), buffered)
  else:
    result = newTSocket(posix.socket(toInt(domain), toInt(typ), toInt(protocol)), buffered)

when defined(ssl):
  CRYPTO_malloc_init()
  SslLibraryInit()
  SslLoadErrorStrings()
  ErrLoadBioStrings()
  OpenSSL_add_all_algorithms()

  proc raiseSSLError(s = "") =
    if s != "":
      raise newException(SSLError, s)
    let err = ErrPeekLastError()
    if err == 0:
      raise newException(SSLError, "No error reported.")
    if err == -1:
      raiseOSError(osLastError())
    var errStr = ErrErrorString(err, nil)
    raise newException(SSLError, $errStr)

  # http://simplestcodings.blogspot.co.uk/2010/08/secure-server-client-using-openssl-in-c.html
  proc loadCertificates(ctx: SSL_CTX, certFile, keyFile: string) =
    if certFile != "" and not existsFile(certFile):
      raise newException(system.IOError, "Certificate file could not be found: " & certFile)
    if keyFile != "" and not existsFile(keyFile):
      raise newException(system.IOError, "Key file could not be found: " & keyFile)
    
    if certFile != "":
      var ret = SSLCTXUseCertificateChainFile(ctx, certFile)
      if ret != 1:
        raiseSslError()
    
    # TODO: Password? www.rtfm.com/openssl-examples/part1.pdf
    if keyFile != "":
      if SSL_CTX_use_PrivateKey_file(ctx, keyFile,
                                     SSL_FILETYPE_PEM) != 1:
        raiseSslError()
        
      if SSL_CTX_check_private_key(ctx) != 1:
        raiseSslError("Verification of private key file failed.")

  proc newContext*(protVersion = protSSLv23, verifyMode = CVerifyPeer,
                   certFile = "", keyFile = ""): SSLContext =
    ## Creates an SSL context.
    ## 
    ## Protocol version specifies the protocol to use. SSLv2, SSLv3, TLSv1 are 
    ## are available with the addition of ``ProtSSLv23`` which allows for 
    ## compatibility with all of them.
    ##
    ## There are currently only two options for verify mode;
    ## one is ``CVerifyNone`` and with it certificates will not be verified
    ## the other is ``CVerifyPeer`` and certificates will be verified for
    ## it, ``CVerifyPeer`` is the safest choice.
    ##
    ## The last two parameters specify the certificate file path and the key file
    ## path, a server socket will most likely not work without these.
    ## Certificates can be generated using the following command:
    ## ``openssl req -x509 -nodes -days 365 -newkey rsa:1024 -keyout mycert.pem -out mycert.pem``.
    var newCTX: SSL_CTX
    case protVersion
    of protSSLv23:
      newCTX = SSL_CTX_new(SSLv23_method()) # SSlv2,3 and TLS1 support.
    of protSSLv2:
      when not defined(linux) and not defined(OpenBSD):
        newCTX = SSL_CTX_new(SSLv2_method())
      else:
        raiseSslError()
    of protSSLv3:
      newCTX = SSL_CTX_new(SSLv3_method())
    of protTLSv1:
      newCTX = SSL_CTX_new(TLSv1_method())
    
    if newCTX.SSLCTXSetCipherList("ALL") != 1:
      raiseSslError()
    case verifyMode
    of CVerifyPeer:
      newCTX.SSLCTXSetVerify(SSLVerifyPeer, nil)
    of CVerifyNone:
      newCTX.SSLCTXSetVerify(SSLVerifyNone, nil)
    if newCTX == nil:
      raiseSslError()

    discard newCTX.SSLCTXSetMode(SSL_MODE_AUTO_RETRY)
    newCTX.loadCertificates(certFile, keyFile)
    return SSLContext(newCTX)

  proc wrapSocket*(ctx: SSLContext, socket: Socket) =
    ## Wraps a socket in an SSL context. This function effectively turns
    ## ``socket`` into an SSL socket.
    ##
    ## **Disclaimer**: This code is not well tested, may be very unsafe and
    ## prone to security vulnerabilities.
    
    socket.isSSL = true
    socket.sslContext = ctx
    socket.sslHandle = SSLNew(SSLCTX(socket.sslContext))
    socket.sslNoHandshake = false
    socket.sslHasPeekChar = false
    if socket.sslHandle == nil:
      raiseSslError()
    
    if SSLSetFd(socket.sslHandle, socket.fd) != 1:
      raiseSslError()

proc raiseSocketError*(socket: Socket, err: int = -1, async = false) =
  ## Raises proper errors based on return values of ``recv`` functions.
  ##
  ## If ``async`` is ``True`` no error will be thrown in the case when the
  ## error was caused by no data being available to be read.
  ##
  ## If ``err`` is not lower than 0 no exception will be raised.
  when defined(ssl):
    if socket.isSSL:
      if err <= 0:
        var ret = SSLGetError(socket.sslHandle, err.cint)
        case ret
        of SSL_ERROR_ZERO_RETURN:
          raiseSslError("TLS/SSL connection failed to initiate, socket closed prematurely.")
        of SSL_ERROR_WANT_CONNECT, SSL_ERROR_WANT_ACCEPT:
          if async:
            return
          else: raiseSslError("Not enough data on socket.")
        of SSL_ERROR_WANT_WRITE, SSL_ERROR_WANT_READ:
          if async:
            return
          else: raiseSslError("Not enough data on socket.")
        of SSL_ERROR_WANT_X509_LOOKUP:
          raiseSslError("Function for x509 lookup has been called.")
        of SSL_ERROR_SYSCALL, SSL_ERROR_SSL:
          raiseSslError()
        else: raiseSslError("Unknown Error")
  
  if err == -1 and not (when defined(ssl): socket.isSSL else: false):
    let lastError = osLastError()
    if async:
      when defined(windows):
        if lastError.int32 == WSAEWOULDBLOCK:
          return
        else: raiseOSError(lastError)
      else:
        if lastError.int32 == EAGAIN or lastError.int32 == EWOULDBLOCK:
          return
        else: raiseOSError(lastError)
    else: raiseOSError(lastError)

proc listen*(socket: Socket, backlog = SOMAXCONN) {.tags: [ReadIOEffect].} =
  ## Marks ``socket`` as accepting connections. 
  ## ``Backlog`` specifies the maximum length of the 
  ## queue of pending connections.
  if listen(socket.fd, cint(backlog)) < 0'i32: raiseOSError(osLastError())

proc invalidIp4(s: string) {.noreturn, noinline.} =
  raise newException(ValueError, "invalid ip4 address: " & s)

proc parseIp4*(s: string): BiggestInt = 
  ## parses an IP version 4 in dotted decimal form like "a.b.c.d".
  ##
  ## This is equivalent to `inet_ntoa`:idx:.
  ##
  ## Raises EInvalidValue in case of an error.
  var a, b, c, d: int
  var i = 0
  var j = parseInt(s, a, i)
  if j <= 0: invalidIp4(s)
  inc(i, j)
  if s[i] == '.': inc(i)
  else: invalidIp4(s)
  j = parseInt(s, b, i)
  if j <= 0: invalidIp4(s)
  inc(i, j)
  if s[i] == '.': inc(i)
  else: invalidIp4(s)
  j = parseInt(s, c, i)
  if j <= 0: invalidIp4(s)
  inc(i, j)
  if s[i] == '.': inc(i)
  else: invalidIp4(s)
  j = parseInt(s, d, i)
  if j <= 0: invalidIp4(s)
  inc(i, j)
  if s[i] != '\0': invalidIp4(s)
  result = BiggestInt(a shl 24 or b shl 16 or c shl 8 or d)

template gaiNim(a, p, h, list: expr): stmt =
  block:
    var gaiResult = getaddrinfo(a, $p, addr(h), list)
    if gaiResult != 0'i32:
      when defined(windows):
        raiseOSError(osLastError())
      else:
        raise newException(OSError, $gai_strerror(gaiResult))

proc bindAddr*(socket: Socket, port = Port(0), address = "") {.
  tags: [ReadIOEffect].} =
  ## binds an address/port number to a socket.
  ## Use address string in dotted decimal form like "a.b.c.d"
  ## or leave "" for any address.

  if address == "":
    var name: Sockaddr_in
    when defined(Windows):
      name.sin_family = int16(ord(AF_INET))
    else:
      name.sin_family = posix.AF_INET
    name.sin_port = sockets.htons(int16(port))
    name.sin_addr.s_addr = sockets.htonl(INADDR_ANY)
    if bindSocket(socket.fd, cast[ptr SockAddr](addr(name)),
                  sizeof(name).SockLen) < 0'i32:
      raiseOSError(osLastError())
  else:
    var hints: AddrInfo
    var aiList: ptr AddrInfo = nil
    hints.ai_family = toInt(AF_INET)
    hints.ai_socktype = toInt(SOCK_STREAM)
    hints.ai_protocol = toInt(IPPROTO_TCP)
    gaiNim(address, port, hints, aiList)
    if bindSocket(socket.fd, aiList.ai_addr, aiList.ai_addrlen.SockLen) < 0'i32:
      raiseOSError(osLastError())
  
proc getSockName*(socket: Socket): Port = 
  ## returns the socket's associated port number.
  var name: Sockaddr_in
  when defined(Windows):
    name.sin_family = int16(ord(AF_INET))
  else:
    name.sin_family = posix.AF_INET
  #name.sin_port = htons(cint16(port))
  #name.sin_addr.s_addr = htonl(INADDR_ANY)
  var namelen = sizeof(name).SockLen
  if getsockname(socket.fd, cast[ptr SockAddr](addr(name)),
                 addr(namelen)) == -1'i32:
    raiseOSError(osLastError())
  result = Port(sockets.ntohs(name.sin_port))

template acceptAddrPlain(noClientRet, successRet: expr, 
                         sslImplementation: stmt): stmt {.immediate.} =
  assert(client != nil)
  var sockAddress: Sockaddr_in
  var addrLen = sizeof(sockAddress).SockLen
  var sock = accept(server.fd, cast[ptr SockAddr](addr(sockAddress)),
                    addr(addrLen))
  
  if sock == osInvalidSocket:
    let err = osLastError()
    when defined(windows):
      if err.int32 == WSAEINPROGRESS:
        client = invalidSocket
        address = ""
        when noClientRet.int == -1:
          return
        else:
          return noClientRet
      else: raiseOSError(err)
    else:
      if err.int32 == EAGAIN or err.int32 == EWOULDBLOCK:
        client = invalidSocket
        address = ""
        when noClientRet.int == -1:
          return
        else:
          return noClientRet
      else: raiseOSError(err)
  else:
    client.fd = sock
    client.isBuffered = server.isBuffered
    sslImplementation
    # Client socket is set above.
    address = $inet_ntoa(sockAddress.sin_addr)
    when successRet.int == -1:
      return
    else:
      return successRet

proc acceptAddr*(server: Socket, client: var Socket, address: var string) {.
  tags: [ReadIOEffect].} =
  ## Blocks until a connection is being made from a client. When a connection
  ## is made sets ``client`` to the client socket and ``address`` to the address
  ## of the connecting client.
  ## If ``server`` is non-blocking then this function returns immediately, and
  ## if there are no connections queued the returned socket will be
  ## ``InvalidSocket``.
  ## This function will raise EOS if an error occurs.
  ##
  ## The resulting client will inherit any properties of the server socket. For
  ## example: whether the socket is buffered or not.
  ##
  ## **Note**: ``client`` must be initialised (with ``new``), this function 
  ## makes no effort to initialise the ``client`` variable.
  ##
  ## **Warning:** When using SSL with non-blocking sockets, it is best to use
  ## the acceptAddrSSL procedure as this procedure will most likely block.
  acceptAddrPlain(-1, -1):
    when defined(ssl):
      if server.isSSL:
        # We must wrap the client sock in a ssl context.
        
        server.sslContext.wrapSocket(client)
        let ret = SSLAccept(client.sslHandle)
        while ret <= 0:
          let err = SSLGetError(client.sslHandle, ret)
          if err != SSL_ERROR_WANT_ACCEPT:
            case err
            of SSL_ERROR_ZERO_RETURN:
              raiseSslError("TLS/SSL connection failed to initiate, socket closed prematurely.")
            of SSL_ERROR_WANT_READ, SSL_ERROR_WANT_WRITE,
               SSL_ERROR_WANT_CONNECT, SSL_ERROR_WANT_ACCEPT:
              raiseSslError("acceptAddrSSL should be used for non-blocking SSL sockets.")
            of SSL_ERROR_WANT_X509_LOOKUP:
              raiseSslError("Function for x509 lookup has been called.")
            of SSL_ERROR_SYSCALL, SSL_ERROR_SSL:
              raiseSslError()
            else:
              raiseSslError("Unknown error")

proc setBlocking*(s: Socket, blocking: bool) {.tags: [], gcsafe.}
  ## Sets blocking mode on socket

when defined(ssl):
  proc acceptAddrSSL*(server: Socket, client: var Socket,
                      address: var string): SSLAcceptResult {.
                      tags: [ReadIOEffect].} =
    ## This procedure should only be used for non-blocking **SSL** sockets. 
    ## It will immediately return with one of the following values:
    ## 
    ## ``AcceptSuccess`` will be returned when a client has been successfully
    ## accepted and the handshake has been successfully performed between
    ## ``server`` and the newly connected client.
    ##
    ## ``AcceptNoHandshake`` will be returned when a client has been accepted
    ## but no handshake could be performed. This can happen when the client
    ## connects but does not yet initiate a handshake. In this case
    ## ``acceptAddrSSL`` should be called again with the same parameters.
    ##
    ## ``AcceptNoClient`` will be returned when no client is currently attempting
    ## to connect.
    template doHandshake(): stmt =
      when defined(ssl):
        if server.isSSL:
          client.setBlocking(false)
          # We must wrap the client sock in a ssl context.
          
          if not client.isSSL or client.sslHandle == nil:
            server.sslContext.wrapSocket(client)
          let ret = SSLAccept(client.sslHandle)
          while ret <= 0:
            let err = SSLGetError(client.sslHandle, ret)
            if err != SSL_ERROR_WANT_ACCEPT:
              case err
              of SSL_ERROR_ZERO_RETURN:
                raiseSslError("TLS/SSL connection failed to initiate, socket closed prematurely.")
              of SSL_ERROR_WANT_READ, SSL_ERROR_WANT_WRITE,
                 SSL_ERROR_WANT_CONNECT, SSL_ERROR_WANT_ACCEPT:
                client.sslNoHandshake = true
                return AcceptNoHandshake
              of SSL_ERROR_WANT_X509_LOOKUP:
                raiseSslError("Function for x509 lookup has been called.")
              of SSL_ERROR_SYSCALL, SSL_ERROR_SSL:
                raiseSslError()
              else:
                raiseSslError("Unknown error")
          client.sslNoHandshake = false

    if client.isSSL and client.sslNoHandshake:
      doHandshake()
      return AcceptSuccess
    else:
      acceptAddrPlain(AcceptNoClient, AcceptSuccess):
        doHandshake()

proc accept*(server: Socket, client: var Socket) {.tags: [ReadIOEffect].} =
  ## Equivalent to ``acceptAddr`` but doesn't return the address, only the
  ## socket.
  ## 
  ## **Note**: ``client`` must be initialised (with ``new``), this function
  ## makes no effort to initialise the ``client`` variable.
  
  var addrDummy = ""
  acceptAddr(server, client, addrDummy)

proc acceptAddr*(server: Socket): tuple[client: Socket, address: string] {.
  deprecated, tags: [ReadIOEffect].} =
  ## Slightly different version of ``acceptAddr``.
  ##
  ## **Deprecated since version 0.9.0:** Please use the function above.
  var client: Socket
  new(client)
  var address = ""
  acceptAddr(server, client, address)
  return (client, address)

proc accept*(server: Socket): Socket {.deprecated, tags: [ReadIOEffect].} =
  ## **Deprecated since version 0.9.0:** Please use the function above.
  new(result)
  var address = ""
  acceptAddr(server, result, address)

proc close*(socket: Socket) =
  ## closes a socket.
  when defined(windows):
    discard winlean.closesocket(socket.fd)
  else:
    discard posix.close(socket.fd)
  # TODO: These values should not be discarded. An EOS should be raised.
  # http://stackoverflow.com/questions/12463473/what-happens-if-you-call-close-on-a-bsd-socket-multiple-times
  when defined(ssl):
    if socket.isSSL:
      discard SSLShutdown(socket.sslHandle)
      SSLFree(socket.sslHandle)
      socket.sslHandle = nil

proc getServByName*(name, proto: string): Servent {.tags: [ReadIOEffect].} =
  ## Searches the database from the beginning and finds the first entry for 
  ## which the service name specified by ``name`` matches the s_name member
  ## and the protocol name specified by ``proto`` matches the s_proto member.
  ##
  ## On posix this will search through the ``/etc/services`` file.
  when defined(Windows):
    var s = winlean.getservbyname(name, proto)
  else:
    var s = posix.getservbyname(name, proto)
  if s == nil: raise newException(OSError, "Service not found.")
  result.name = $s.s_name
  result.aliases = cstringArrayToSeq(s.s_aliases)
  result.port = Port(s.s_port)
  result.proto = $s.s_proto
  
proc getServByPort*(port: Port, proto: string): Servent {.tags: [ReadIOEffect].} = 
  ## Searches the database from the beginning and finds the first entry for 
  ## which the port specified by ``port`` matches the s_port member and the 
  ## protocol name specified by ``proto`` matches the s_proto member.
  ##
  ## On posix this will search through the ``/etc/services`` file.
  when defined(Windows):
    var s = winlean.getservbyport(ze(int16(port)).cint, proto)
  else:
    var s = posix.getservbyport(ze(int16(port)).cint, proto)
  if s == nil: raise newException(OSError, "Service not found.")
  result.name = $s.s_name
  result.aliases = cstringArrayToSeq(s.s_aliases)
  result.port = Port(s.s_port)
  result.proto = $s.s_proto

proc getHostByAddr*(ip: string): Hostent {.tags: [ReadIOEffect].} =
  ## This function will lookup the hostname of an IP Address.
  var myaddr: InAddr
  myaddr.s_addr = inet_addr(ip)
  
  when defined(windows):
    var s = winlean.gethostbyaddr(addr(myaddr), sizeof(myaddr).cuint,
                                  cint(sockets.AF_INET))
    if s == nil: raiseOSError(osLastError())
  else:
    var s = posix.gethostbyaddr(addr(myaddr), sizeof(myaddr).Socklen, 
                                cint(posix.AF_INET))
    if s == nil:
      raise newException(OSError, $hstrerror(h_errno))
  
  result.name = $s.h_name
  result.aliases = cstringArrayToSeq(s.h_aliases)
  when defined(windows): 
    result.addrtype = Domain(s.h_addrtype)
  else:
    if s.h_addrtype == posix.AF_INET:
      result.addrtype = AF_INET
    elif s.h_addrtype == posix.AF_INET6:
      result.addrtype = AF_INET6
    else:
      raise newException(OSError, "unknown h_addrtype")
  result.addrList = cstringArrayToSeq(s.h_addr_list)
  result.length = int(s.h_length)

proc getHostByName*(name: string): Hostent {.tags: [ReadIOEffect].} = 
  ## This function will lookup the IP address of a hostname.
  when defined(Windows):
    var s = winlean.gethostbyname(name)
  else:
    var s = posix.gethostbyname(name)
  if s == nil: raiseOSError(osLastError())
  result.name = $s.h_name
  result.aliases = cstringArrayToSeq(s.h_aliases)
  when defined(windows): 
    result.addrtype = Domain(s.h_addrtype)
  else:
    if s.h_addrtype == posix.AF_INET:
      result.addrtype = AF_INET
    elif s.h_addrtype == posix.AF_INET6:
      result.addrtype = AF_INET6
    else:
      raise newException(OSError, "unknown h_addrtype")
  result.addrList = cstringArrayToSeq(s.h_addr_list)
  result.length = int(s.h_length)

proc getSockOptInt*(socket: Socket, level, optname: int): int {.
  tags: [ReadIOEffect].} = 
  ## getsockopt for integer options.
  var res: cint
  var size = sizeof(res).SockLen
  if getsockopt(socket.fd, cint(level), cint(optname), 
                addr(res), addr(size)) < 0'i32:
    raiseOSError(osLastError())
  result = int(res)

proc setSockOptInt*(socket: Socket, level, optname, optval: int) {.
  tags: [WriteIOEffect].} =
  ## setsockopt for integer options.
  var value = cint(optval)
  if setsockopt(socket.fd, cint(level), cint(optname), addr(value),  
                sizeof(value).SockLen) < 0'i32:
    raiseOSError(osLastError())

proc toCInt(opt: SOBool): cint =
  case opt
  of OptAcceptConn: SO_ACCEPTCONN
  of OptBroadcast: SO_BROADCAST
  of OptDebug: SO_DEBUG
  of OptDontRoute: SO_DONTROUTE
  of OptKeepAlive: SO_KEEPALIVE
  of OptOOBInline: SO_OOBINLINE
  of OptReuseAddr: SO_REUSEADDR

proc getSockOpt*(socket: Socket, opt: SOBool, level = SOL_SOCKET): bool {.
  tags: [ReadIOEffect].} =
  ## Retrieves option ``opt`` as a boolean value.
  var res: cint
  var size = sizeof(res).SockLen
  if getsockopt(socket.fd, cint(level), toCInt(opt), 
                addr(res), addr(size)) < 0'i32:
    raiseOSError(osLastError())
  result = res != 0

proc setSockOpt*(socket: Socket, opt: SOBool, value: bool, level = SOL_SOCKET) {.
  tags: [WriteIOEffect].} =
  ## Sets option ``opt`` to a boolean value specified by ``value``.
  var valuei = cint(if value: 1 else: 0)
  if setsockopt(socket.fd, cint(level), toCInt(opt), addr(valuei),  
                sizeof(valuei).SockLen) < 0'i32:
    raiseOSError(osLastError())

proc connect*(socket: Socket, address: string, port = Port(0), 
              af: Domain = AF_INET) {.tags: [ReadIOEffect].} =
  ## Connects socket to ``address``:``port``. ``Address`` can be an IP address or a
  ## host name. If ``address`` is a host name, this function will try each IP
  ## of that host name. ``htons`` is already performed on ``port`` so you must
  ## not do it.
  ##
  ## If ``socket`` is an SSL socket a handshake will be automatically performed.
  var hints: AddrInfo
  var aiList: ptr AddrInfo = nil
  hints.ai_family = toInt(af)
  hints.ai_socktype = toInt(SOCK_STREAM)
  hints.ai_protocol = toInt(IPPROTO_TCP)
  gaiNim(address, port, hints, aiList)
  # try all possibilities:
  var success = false
  var lastError: OSErrorCode
  var it = aiList
  while it != nil:
    if connect(socket.fd, it.ai_addr, it.ai_addrlen.SockLen) == 0'i32:
      success = true
      break
    else: lastError = osLastError()
    it = it.ai_next

  freeaddrinfo(aiList)
  if not success: raiseOSError(lastError)
  
  when defined(ssl):
    if socket.isSSL:
      let ret = SSLConnect(socket.sslHandle)
      if ret <= 0:
        let err = SSLGetError(socket.sslHandle, ret)
        case err
        of SSL_ERROR_ZERO_RETURN:
          raiseSslError("TLS/SSL connection failed to initiate, socket closed prematurely.")
        of SSL_ERROR_WANT_READ, SSL_ERROR_WANT_WRITE, SSL_ERROR_WANT_CONNECT, 
           SSL_ERROR_WANT_ACCEPT:
          raiseSslError("The operation did not complete. Perhaps you should use connectAsync?")
        of SSL_ERROR_WANT_X509_LOOKUP:
          raiseSslError("Function for x509 lookup has been called.")
        of SSL_ERROR_SYSCALL, SSL_ERROR_SSL:
          raiseSslError()
        else:
          raiseSslError("Unknown error")
        
  when false:
    var s: TSockAddrIn
    s.sin_addr.s_addr = inet_addr(address)
    s.sin_port = sockets.htons(int16(port))
    when defined(windows):
      s.sin_family = toU16(ord(af))
    else:
      case af 
      of AF_UNIX: s.sin_family = posix.AF_UNIX
      of AF_INET: s.sin_family = posix.AF_INET
      of AF_INET6: s.sin_family = posix.AF_INET6
      else: nil
    if connect(socket.fd, cast[ptr TSockAddr](addr(s)), sizeof(s).cint) < 0'i32:
      OSError()

proc connectAsync*(socket: Socket, name: string, port = Port(0),
                     af: Domain = AF_INET) {.tags: [ReadIOEffect].} =
  ## A variant of ``connect`` for non-blocking sockets.
  ##
  ## This procedure will immediately return, it will not block until a connection
  ## is made. It is up to the caller to make sure the connection has been established
  ## by checking (using ``select``) whether the socket is writeable.
  ##
  ## **Note**: For SSL sockets, the ``handshake`` procedure must be called
  ## whenever the socket successfully connects to a server.
  var hints: AddrInfo
  var aiList: ptr AddrInfo = nil
  hints.ai_family = toInt(af)
  hints.ai_socktype = toInt(SOCK_STREAM)
  hints.ai_protocol = toInt(IPPROTO_TCP)
  gaiNim(name, port, hints, aiList)
  # try all possibilities:
  var success = false
  var lastError: OSErrorCode
  var it = aiList
  while it != nil:
    var ret = connect(socket.fd, it.ai_addr, it.ai_addrlen.SockLen)
    if ret == 0'i32:
      success = true
      break
    else:
      lastError = osLastError()
      when defined(windows):
        # Windows EINTR doesn't behave same as POSIX.
        if lastError.int32 == WSAEWOULDBLOCK:
          success = true
          break
      else:
        if lastError.int32 == EINTR or lastError.int32 == EINPROGRESS:
          success = true
          break
        
    it = it.ai_next

  freeaddrinfo(aiList)
  if not success: raiseOSError(lastError)
  when defined(ssl):
    if socket.isSSL:
      socket.sslNoHandshake = true

when defined(ssl):
  proc handshake*(socket: Socket): bool {.tags: [ReadIOEffect, WriteIOEffect].} =
    ## This proc needs to be called on a socket after it connects. This is
    ## only applicable when using ``connectAsync``.
    ## This proc performs the SSL handshake.
    ##
    ## Returns ``False`` whenever the socket is not yet ready for a handshake,
    ## ``True`` whenever handshake completed successfully.
    ##
    ## A ESSL error is raised on any other errors.
    result = true
    if socket.isSSL:
      var ret = SSLConnect(socket.sslHandle)
      if ret <= 0:
        var errret = SSLGetError(socket.sslHandle, ret)
        case errret
        of SSL_ERROR_ZERO_RETURN:
          raiseSslError("TLS/SSL connection failed to initiate, socket closed prematurely.")
        of SSL_ERROR_WANT_CONNECT, SSL_ERROR_WANT_ACCEPT,
          SSL_ERROR_WANT_READ, SSL_ERROR_WANT_WRITE:
          return false
        of SSL_ERROR_WANT_X509_LOOKUP:
          raiseSslError("Function for x509 lookup has been called.")
        of SSL_ERROR_SYSCALL, SSL_ERROR_SSL:
          raiseSslError()
        else:
          raiseSslError("Unknown Error")
      socket.sslNoHandshake = false
    else:
      raiseSslError("Socket is not an SSL socket.")

  proc gotHandshake*(socket: Socket): bool =
    ## Determines whether a handshake has occurred between a client (``socket``)
    ## and the server that ``socket`` is connected to.
    ##
    ## Throws ESSL if ``socket`` is not an SSL socket.
    if socket.isSSL:
      return not socket.sslNoHandshake
    else:
      raiseSslError("Socket is not an SSL socket.")

proc timeValFromMilliseconds(timeout = 500): Timeval =
  if timeout != -1:
    var seconds = timeout div 1000
    result.tv_sec = seconds.int32
    result.tv_usec = ((timeout - seconds * 1000) * 1000).int32

proc createFdSet(fd: var TFdSet, s: seq[Socket], m: var int) = 
  FD_ZERO(fd)
  for i in items(s): 
    m = max(m, int(i.fd))
    FD_SET(i.fd, fd)
   
proc pruneSocketSet(s: var seq[Socket], fd: var TFdSet) =
  var i = 0
  var L = s.len
  while i < L:
    if FD_ISSET(s[i].fd, fd) == 0'i32:
      # not set.
      s[i] = s[L-1]
      dec(L)
    else:
      inc(i)
  setLen(s, L)

proc hasDataBuffered*(s: Socket): bool =
  ## Determines whether a socket has data buffered.
  result = false
  if s.isBuffered:
    result = s.bufLen > 0 and s.currPos != s.bufLen

  when defined(ssl):
    if s.isSSL and not result:
      result = s.sslHasPeekChar

proc checkBuffer(readfds: var seq[Socket]): int =
  ## Checks the buffer of each socket in ``readfds`` to see whether there is data.
  ## Removes the sockets from ``readfds`` and returns the count of removed sockets.
  var res: seq[Socket] = @[]
  result = 0
  for s in readfds:
    if hasDataBuffered(s):
      inc(result)
      res.add(s)
  if result > 0:
    readfds = res

proc select*(readfds, writefds, exceptfds: var seq[Socket], 
             timeout = 500): int {.tags: [ReadIOEffect].} = 
  ## Traditional select function. This function will return the number of
  ## sockets that are ready to be read from, written to, or which have errors.
  ## If there are none; 0 is returned. 
  ## ``Timeout`` is in miliseconds and -1 can be specified for no timeout.
  ## 
  ## Sockets which are **not** ready for reading, writing or which don't have
  ## errors waiting on them are removed from the ``readfds``, ``writefds``,
  ## ``exceptfds`` sequences respectively.
  let buffersFilled = checkBuffer(readfds)
  if buffersFilled > 0:
    return buffersFilled

  var tv {.noInit.}: Timeval = timeValFromMilliseconds(timeout)
  
  var rd, wr, ex: TFdSet
  var m = 0
  createFdSet((rd), readfds, m)
  createFdSet((wr), writefds, m)
  createFdSet((ex), exceptfds, m)
  
  if timeout != -1:
    result = int(select(cint(m+1), addr(rd), addr(wr), addr(ex), addr(tv)))
  else:
    result = int(select(cint(m+1), addr(rd), addr(wr), addr(ex), nil))
  
  pruneSocketSet(readfds, (rd))
  pruneSocketSet(writefds, (wr))
  pruneSocketSet(exceptfds, (ex))

proc select*(readfds, writefds: var seq[Socket], 
             timeout = 500): int {.tags: [ReadIOEffect].} =
  ## Variant of select with only a read and write list.
  let buffersFilled = checkBuffer(readfds)
  if buffersFilled > 0:
    return buffersFilled
  var tv {.noInit.}: Timeval = timeValFromMilliseconds(timeout)
  
  var rd, wr: TFdSet
  var m = 0
  createFdSet((rd), readfds, m)
  createFdSet((wr), writefds, m)
  
  if timeout != -1:
    result = int(select(cint(m+1), addr(rd), addr(wr), nil, addr(tv)))
  else:
    result = int(select(cint(m+1), addr(rd), addr(wr), nil, nil))
  
  pruneSocketSet(readfds, (rd))
  pruneSocketSet(writefds, (wr))

proc selectWrite*(writefds: var seq[Socket], 
                  timeout = 500): int {.tags: [ReadIOEffect].} =
  ## When a socket in ``writefds`` is ready to be written to then a non-zero
  ## value will be returned specifying the count of the sockets which can be
  ## written to. The sockets which **cannot** be written to will also be removed
  ## from ``writefds``.
  ##
  ## ``timeout`` is specified in miliseconds and ``-1`` can be specified for
  ## an unlimited time.
  var tv {.noInit.}: Timeval = timeValFromMilliseconds(timeout)
  
  var wr: TFdSet
  var m = 0
  createFdSet((wr), writefds, m)
  
  if timeout != -1:
    result = int(select(cint(m+1), nil, addr(wr), nil, addr(tv)))
  else:
    result = int(select(cint(m+1), nil, addr(wr), nil, nil))
  
  pruneSocketSet(writefds, (wr))

proc select*(readfds: var seq[Socket], timeout = 500): int =
  ## variant of select with a read list only
  let buffersFilled = checkBuffer(readfds)
  if buffersFilled > 0:
    return buffersFilled
  var tv {.noInit.}: Timeval = timeValFromMilliseconds(timeout)
  
  var rd: TFdSet
  var m = 0
  createFdSet((rd), readfds, m)
  
  if timeout != -1:
    result = int(select(cint(m+1), addr(rd), nil, nil, addr(tv)))
  else:
    result = int(select(cint(m+1), addr(rd), nil, nil, nil))
  
  pruneSocketSet(readfds, (rd))

proc readIntoBuf(socket: Socket, flags: int32): int =
  result = 0
  when defined(ssl):
    if socket.isSSL:
      result = SSLRead(socket.sslHandle, addr(socket.buffer), int(socket.buffer.high))
    else:
      result = recv(socket.fd, addr(socket.buffer), cint(socket.buffer.high), flags)
  else:
    result = recv(socket.fd, addr(socket.buffer), cint(socket.buffer.high), flags)
  if result <= 0:
    socket.bufLen = 0
    socket.currPos = 0
    return result
  socket.bufLen = result
  socket.currPos = 0

template retRead(flags, readBytes: int) {.dirty.} =
  let res = socket.readIntoBuf(flags.int32)
  if res <= 0:
    if readBytes > 0:
      return readBytes
    else:
      return res

proc recv*(socket: Socket, data: pointer, size: int): int {.tags: [ReadIOEffect].} =
  ## Receives data from a socket.
  ##
  ## **Note**: This is a low-level function, you may be interested in the higher
  ## level versions of this function which are also named ``recv``.
  if size == 0: return
  if socket.isBuffered:
    if socket.bufLen == 0:
      retRead(0'i32, 0)
    
    var read = 0
    while read < size:
      if socket.currPos >= socket.bufLen:
        retRead(0'i32, read)
    
      let chunk = min(socket.bufLen-socket.currPos, size-read)
      var d = cast[cstring](data)
      copyMem(addr(d[read]), addr(socket.buffer[socket.currPos]), chunk)
      read.inc(chunk)
      socket.currPos.inc(chunk)

    result = read
  else:
    when defined(ssl):
      if socket.isSSL:
        if socket.sslHasPeekChar:
          copyMem(data, addr(socket.sslPeekChar), 1)
          socket.sslHasPeekChar = false
          if size-1 > 0:
            var d = cast[cstring](data)
            result = SSLRead(socket.sslHandle, addr(d[1]), size-1) + 1
          else:
            result = 1
        else:
          result = SSLRead(socket.sslHandle, data, size)
      else:
        result = recv(socket.fd, data, size.cint, 0'i32)
    else:
      result = recv(socket.fd, data, size.cint, 0'i32)

proc waitFor(socket: Socket, waited: var float, timeout, size: int,
             funcName: string): int {.tags: [TimeEffect].} =
  ## determines the amount of characters that can be read. Result will never
  ## be larger than ``size``. For unbuffered sockets this will be ``1``.
  ## For buffered sockets it can be as big as ``BufferSize``.
  ##
  ## If this function does not determine that there is data on the socket
  ## within ``timeout`` ms, an ETimeout error will be raised.
  result = 1
  if size <= 0: assert false
  if timeout == -1: return size
  if socket.isBuffered and socket.bufLen != 0 and socket.bufLen != socket.currPos:
    result = socket.bufLen - socket.currPos
    result = min(result, size)
  else:
    if timeout - int(waited * 1000.0) < 1:
      raise newException(TimeoutError, "Call to '" & funcName & "' timed out.")
    
    when defined(ssl):
      if socket.isSSL:
        if socket.hasDataBuffered:
          # sslPeekChar is present.
          return 1
        let sslPending = SSLPending(socket.sslHandle)
        if sslPending != 0:
          return sslPending
    
    var s = @[socket]
    var startTime = epochTime()
    let selRet = select(s, timeout - int(waited * 1000.0))
    if selRet < 0: raiseOSError(osLastError())
    if selRet != 1:
      raise newException(TimeoutError, "Call to '" & funcName & "' timed out.")
    waited += (epochTime() - startTime)

proc recv*(socket: Socket, data: pointer, size: int, timeout: int): int {.
  tags: [ReadIOEffect, TimeEffect].} =
  ## overload with a ``timeout`` parameter in miliseconds.
  var waited = 0.0 # number of seconds already waited  
  
  var read = 0
  while read < size:
    let avail = waitFor(socket, waited, timeout, size-read, "recv")
    var d = cast[cstring](data)
    result = recv(socket, addr(d[read]), avail)
    if result == 0: break
    if result < 0:
      return result
    inc(read, result)
  
  result = read

proc recv*(socket: Socket, data: var string, size: int, timeout = -1): int =
  ## Higher-level version of ``recv``.
  ##
  ## When 0 is returned the socket's connection has been closed.
  ##
  ## This function will throw an EOS exception when an error occurs. A value
  ## lower than 0 is never returned.
  ##
  ## A timeout may be specified in miliseconds, if enough data is not received
  ## within the time specified an ETimeout exception will be raised.
  ##
  ## **Note**: ``data`` must be initialised.
  data.setLen(size)
  result = recv(socket, cstring(data), size, timeout)
  if result < 0:
    data.setLen(0)
    socket.raiseSocketError(result)
  data.setLen(result)

proc recvAsync*(socket: Socket, data: var string, size: int): int =
  ## Async version of ``recv``.
  ##
  ## When socket is non-blocking and no data is available on the socket,
  ## ``-1`` will be returned and ``data`` will be ``""``.
  ##
  ## **Note**: ``data`` must be initialised.
  data.setLen(size)
  result = recv(socket, cstring(data), size)
  if result < 0:
    data.setLen(0)
    socket.raiseSocketError(async = true)
    result = -1
  data.setLen(result)

proc peekChar(socket: Socket, c: var char): int {.tags: [ReadIOEffect].} =
  if socket.isBuffered:
    result = 1
    if socket.bufLen == 0 or socket.currPos > socket.bufLen-1:
      var res = socket.readIntoBuf(0'i32)
      if res <= 0:
        result = res
    
    c = socket.buffer[socket.currPos]
  else:
    when defined(ssl):
      if socket.isSSL:
        if not socket.sslHasPeekChar:
          result = SSLRead(socket.sslHandle, addr(socket.sslPeekChar), 1)
          socket.sslHasPeekChar = true
        
        c = socket.sslPeekChar
        return
    result = recv(socket.fd, addr(c), 1, MSG_PEEK)

proc recvLine*(socket: Socket, line: var TaintedString, timeout = -1): bool {.
  tags: [ReadIOEffect, TimeEffect], deprecated.} =
  ## Receive a line of data from ``socket``.
  ##
  ## If a full line is received ``\r\L`` is not
  ## added to ``line``, however if solely ``\r\L`` is received then ``line``
  ## will be set to it.
  ## 
  ## ``True`` is returned if data is available. ``False`` suggests an
  ## error, EOS exceptions are not raised and ``False`` is simply returned
  ## instead.
  ## 
  ## If the socket is disconnected, ``line`` will be set to ``""`` and ``True``
  ## will be returned.
  ##
  ## A timeout can be specified in miliseconds, if data is not received within
  ## the specified time an ETimeout exception will be raised.
  ##
  ## **Deprecated since version 0.9.2**: This function has been deprecated in
  ## favour of readLine.
  
  template addNLIfEmpty(): stmt =
    if line.len == 0:
      line.add("\c\L")

  var waited = 0.0

  setLen(line.string, 0)
  while true:
    var c: char
    discard waitFor(socket, waited, timeout, 1, "recvLine")
    var n = recv(socket, addr(c), 1)
    if n < 0: return
    elif n == 0: return true
    if c == '\r':
      discard waitFor(socket, waited, timeout, 1, "recvLine")
      n = peekChar(socket, c)
      if n > 0 and c == '\L':
        discard recv(socket, addr(c), 1)
      elif n <= 0: return false
      addNLIfEmpty()
      return true
    elif c == '\L': 
      addNLIfEmpty()
      return true
    add(line.string, c)

proc readLine*(socket: Socket, line: var TaintedString, timeout = -1) {.
  tags: [ReadIOEffect, TimeEffect].} =
  ## Reads a line of data from ``socket``.
  ##
  ## If a full line is read ``\r\L`` is not
  ## added to ``line``, however if solely ``\r\L`` is read then ``line``
  ## will be set to it.
  ## 
  ## If the socket is disconnected, ``line`` will be set to ``""``.
  ##
  ## An EOS exception will be raised in the case of a socket error.
  ##
  ## A timeout can be specified in miliseconds, if data is not received within
  ## the specified time an ETimeout exception will be raised.
  
  template addNLIfEmpty(): stmt =
    if line.len == 0:
      line.add("\c\L")

  var waited = 0.0

  setLen(line.string, 0)
  while true:
    var c: char
    discard waitFor(socket, waited, timeout, 1, "readLine")
    var n = recv(socket, addr(c), 1)
    if n < 0: socket.raiseSocketError()
    elif n == 0: return
    if c == '\r':
      discard waitFor(socket, waited, timeout, 1, "readLine")
      n = peekChar(socket, c)
      if n > 0 and c == '\L':
        discard recv(socket, addr(c), 1)
      elif n <= 0: socket.raiseSocketError()
      addNLIfEmpty()
      return
    elif c == '\L': 
      addNLIfEmpty()
      return
    add(line.string, c)

proc recvLineAsync*(socket: Socket, 
  line: var TaintedString): RecvLineResult {.tags: [ReadIOEffect], deprecated.} =
  ## Similar to ``recvLine`` but designed for non-blocking sockets.
  ##
  ## The values of the returned enum should be pretty self explanatory:
  ##
  ##   * If a full line has been retrieved; ``RecvFullLine`` is returned.
  ##   * If some data has been retrieved; ``RecvPartialLine`` is returned.
  ##   * If the socket has been disconnected; ``RecvDisconnected`` is returned.
  ##   * If call to ``recv`` failed; ``RecvFail`` is returned.
  ##
  ## **Deprecated since version 0.9.2**: This function has been deprecated in
  ## favour of readLineAsync.

  setLen(line.string, 0)
  while true:
    var c: char
    var n = recv(socket, addr(c), 1)
    if n < 0: 
      return (if line.len == 0: RecvFail else: RecvPartialLine)
    elif n == 0: 
      return (if line.len == 0: RecvDisconnected else: RecvPartialLine)
    if c == '\r':
      n = peekChar(socket, c)
      if n > 0 and c == '\L':
        discard recv(socket, addr(c), 1)
      elif n <= 0: 
        return (if line.len == 0: RecvFail else: RecvPartialLine)
      return RecvFullLine
    elif c == '\L': return RecvFullLine
    add(line.string, c)

proc readLineAsync*(socket: Socket, 
  line: var TaintedString): ReadLineResult {.tags: [ReadIOEffect].} =
  ## Similar to ``recvLine`` but designed for non-blocking sockets.
  ##
  ## The values of the returned enum should be pretty self explanatory:
  ##
  ##   * If a full line has been retrieved; ``ReadFullLine`` is returned.
  ##   * If some data has been retrieved; ``ReadPartialLine`` is returned.
  ##   * If the socket has been disconnected; ``ReadDisconnected`` is returned.
  ##   * If no data could be retrieved; ``ReadNone`` is returned.
  ##   * If call to ``recv`` failed; **an EOS exception is raised.**
  setLen(line.string, 0)
  
  template errorOrNone =
    socket.raiseSocketError(async = true)
    return ReadNone
  
  while true:
    var c: char
    var n = recv(socket, addr(c), 1)
    #echo(n)
    if n < 0:
      if line.len == 0: errorOrNone else: return ReadPartialLine
    elif n == 0: 
      return (if line.len == 0: ReadDisconnected else: ReadPartialLine)
    if c == '\r':
      n = peekChar(socket, c)
      if n > 0 and c == '\L':
        discard recv(socket, addr(c), 1)
      elif n <= 0: 
        if line.len == 0: errorOrNone else: return ReadPartialLine
      return ReadFullLine
    elif c == '\L': return ReadFullLine
    add(line.string, c)

proc recv*(socket: Socket): TaintedString {.tags: [ReadIOEffect], deprecated.} =
  ## receives all the available data from the socket.
  ## Socket errors will result in an ``EOS`` error.
  ## If socket is not a connectionless socket and socket is not connected
  ## ``""`` will be returned.
  ##
  ## **Deprecated since version 0.9.2**: This function is not safe for use.
  const bufSize = 4000
  result = newStringOfCap(bufSize).TaintedString
  var pos = 0
  while true:
    var bytesRead = recv(socket, addr(string(result)[pos]), bufSize-1)
    if bytesRead == -1: raiseOSError(osLastError())
    setLen(result.string, pos + bytesRead)
    if bytesRead != bufSize-1: break
    # increase capacity:
    setLen(result.string, result.string.len + bufSize)
    inc(pos, bytesRead)
  when false:
    var buf = newString(bufSize)
    result = TaintedString""
    while true:
      var bytesRead = recv(socket, cstring(buf), bufSize-1)
      # Error
      if bytesRead == -1: OSError(osLastError())
      
      buf[bytesRead] = '\0' # might not be necessary
      setLen(buf, bytesRead)
      add(result.string, buf)
      if bytesRead != bufSize-1: break

{.push warning[deprecated]: off.}
proc recvTimeout*(socket: Socket, timeout: int): TaintedString {.
  tags: [ReadIOEffect], deprecated.} =
  ## overloaded variant to support a ``timeout`` parameter, the ``timeout``
  ## parameter specifies the amount of miliseconds to wait for data on the
  ## socket.
  ##
  ## **Deprecated since version 0.9.2**: This function is not safe for use.
  if socket.bufLen == 0:
    var s = @[socket]
    if s.select(timeout) != 1:
      raise newException(TimeoutError, "Call to recv() timed out.")
  
  return socket.recv
{.pop.}

proc recvAsync*(socket: Socket, s: var TaintedString): bool {.
  tags: [ReadIOEffect], deprecated.} =
  ## receives all the data from a non-blocking socket. If socket is non-blocking 
  ## and there are no messages available, `False` will be returned.
  ## Other socket errors will result in an ``EOS`` error.
  ## If socket is not a connectionless socket and socket is not connected
  ## ``s`` will be set to ``""``.
  ##
  ## **Deprecated since version 0.9.2**: This function is not safe for use.
  const bufSize = 1000
  # ensure bufSize capacity:
  setLen(s.string, bufSize)
  setLen(s.string, 0)
  var pos = 0
  while true:
    var bytesRead = recv(socket, addr(string(s)[pos]), bufSize-1)
    when defined(ssl):
      if socket.isSSL:
        if bytesRead <= 0:
          var ret = SSLGetError(socket.sslHandle, bytesRead.cint)
          case ret
          of SSL_ERROR_ZERO_RETURN:
            raiseSslError("TLS/SSL connection failed to initiate, socket closed prematurely.")
          of SSL_ERROR_WANT_CONNECT, SSL_ERROR_WANT_ACCEPT:
            raiseSslError("Unexpected error occurred.") # This should just not happen.
          of SSL_ERROR_WANT_WRITE, SSL_ERROR_WANT_READ:
            return false
          of SSL_ERROR_WANT_X509_LOOKUP:
            raiseSslError("Function for x509 lookup has been called.")
          of SSL_ERROR_SYSCALL, SSL_ERROR_SSL:
            raiseSslError()
          else: raiseSslError("Unknown Error")
          
    if bytesRead == -1 and not (when defined(ssl): socket.isSSL else: false):
      let err = osLastError()
      when defined(windows):
        if err.int32 == WSAEWOULDBLOCK:
          return false
        else: raiseOSError(err)
      else:
        if err.int32 == EAGAIN or err.int32 == EWOULDBLOCK:
          return false
        else: raiseOSError(err)

    setLen(s.string, pos + bytesRead)
    if bytesRead != bufSize-1: break
    # increase capacity:
    setLen(s.string, s.string.len + bufSize)
    inc(pos, bytesRead)
  result = true

proc recvFrom*(socket: Socket, data: var string, length: int,
               address: var string, port: var Port, flags = 0'i32): int {.
               tags: [ReadIOEffect].} =
  ## Receives data from ``socket``. This function should normally be used with
  ## connection-less sockets (UDP sockets).
  ##
  ## If an error occurs the return value will be ``-1``. Otherwise the return
  ## value will be the length of data received.
  ##
  ## **Warning:** This function does not yet have a buffered implementation,
  ## so when ``socket`` is buffered the non-buffered implementation will be
  ## used. Therefore if ``socket`` contains something in its buffer this
  ## function will make no effort to return it.
  
  # TODO: Buffered sockets
  data.setLen(length)
  var sockAddress: Sockaddr_in
  var addrLen = sizeof(sockAddress).SockLen
  result = recvfrom(socket.fd, cstring(data), length.cint, flags.cint,
                    cast[ptr SockAddr](addr(sockAddress)), addr(addrLen))

  if result != -1:
    data.setLen(result)
    address = $inet_ntoa(sockAddress.sin_addr)
    port = ntohs(sockAddress.sin_port).Port

proc recvFromAsync*(socket: Socket, data: var string, length: int,
                    address: var string, port: var Port, 
                    flags = 0'i32): bool {.tags: [ReadIOEffect].} =
  ## Variant of ``recvFrom`` for non-blocking sockets. Unlike ``recvFrom``,
  ## this function will raise an EOS error whenever a socket error occurs.
  ##
  ## If there is no data to be read from the socket ``False`` will be returned.
  result = true
  var callRes = recvFrom(socket, data, length, address, port, flags)
  if callRes < 0:
    let err = osLastError()
    when defined(windows):
      if err.int32 == WSAEWOULDBLOCK:
        return false
      else: raiseOSError(err)
    else:
      if err.int32 == EAGAIN or err.int32 == EWOULDBLOCK:
        return false
      else: raiseOSError(err)

proc skip*(socket: Socket) {.tags: [ReadIOEffect], deprecated.} =
  ## skips all the data that is pending for the socket
  ##
  ## **Deprecated since version 0.9.2**: This function is not safe for use.
  const bufSize = 1000
  var buf = alloc(bufSize)
  while recv(socket, buf, bufSize) == bufSize: discard
  dealloc(buf)

proc skip*(socket: Socket, size: int, timeout = -1) =
  ## Skips ``size`` amount of bytes.
  ##
  ## An optional timeout can be specified in miliseconds, if skipping the
  ## bytes takes longer than specified an ETimeout exception will be raised.
  ##
  ## Returns the number of skipped bytes.
  var waited = 0.0
  var dummy = alloc(size)
  var bytesSkipped = 0
  while bytesSkipped != size:
    let avail = waitFor(socket, waited, timeout, size-bytesSkipped, "skip")
    bytesSkipped += recv(socket, dummy, avail)
  dealloc(dummy)

proc send*(socket: Socket, data: pointer, size: int): int {.
  tags: [WriteIOEffect].} =
  ## sends data to a socket.
  when defined(ssl):
    if socket.isSSL:
      return SSLWrite(socket.sslHandle, cast[cstring](data), size)
  
  when defined(windows) or defined(macosx):
    result = send(socket.fd, data, size.cint, 0'i32)
  else:
    when defined(solaris): 
      const MSG_NOSIGNAL = 0
    result = send(socket.fd, data, size, int32(MSG_NOSIGNAL))

proc send*(socket: Socket, data: string) {.tags: [WriteIOEffect].} =
  ## sends data to a socket.
  if socket.nonblocking:
    raise newException(ValueError, "This function cannot be used on non-blocking sockets.")
  let sent = send(socket, cstring(data), data.len)
  if sent < 0:
    when defined(ssl):
      if socket.isSSL:
        raiseSslError()
    
    raiseOSError(osLastError())

  if sent != data.len:
    raise newException(OSError, "Could not send all data.")

proc sendAsync*(socket: Socket, data: string): int {.tags: [WriteIOEffect].} =
  ## sends data to a non-blocking socket.
  ## Returns ``0`` if no data could be sent, if data has been sent
  ## returns the amount of bytes of ``data`` that was successfully sent. This
  ## number may not always be the length of ``data`` but typically is.
  ##
  ## An EOS (or ESSL if socket is an SSL socket) exception is raised if an error
  ## occurs.
  result = send(socket, cstring(data), data.len)
  when defined(ssl):
    if socket.isSSL:
      if result <= 0:
          let ret = SSLGetError(socket.sslHandle, result.cint)
          case ret
          of SSL_ERROR_ZERO_RETURN:
            raiseSslError("TLS/SSL connection failed to initiate, socket closed prematurely.")
          of SSL_ERROR_WANT_CONNECT, SSL_ERROR_WANT_ACCEPT:
            raiseSslError("Unexpected error occurred.") # This should just not happen.
          of SSL_ERROR_WANT_WRITE, SSL_ERROR_WANT_READ:
            return 0
          of SSL_ERROR_WANT_X509_LOOKUP:
            raiseSslError("Function for x509 lookup has been called.")
          of SSL_ERROR_SYSCALL, SSL_ERROR_SSL:
            raiseSslError()
          else: raiseSslError("Unknown Error")
      else:
        return
  if result == -1:
    let err = osLastError()
    when defined(windows):
      if err.int32 == WSAEINPROGRESS:
        return 0
      else: raiseOSError(err)
    else:
      if err.int32 == EAGAIN or err.int32 == EWOULDBLOCK:
        return 0
      else: raiseOSError(err)
  

proc trySend*(socket: Socket, data: string): bool {.tags: [WriteIOEffect].} =
  ## safe alternative to ``send``. Does not raise an EOS when an error occurs,
  ## and instead returns ``false`` on failure.
  result = send(socket, cstring(data), data.len) == data.len

proc sendTo*(socket: Socket, address: string, port: Port, data: pointer,
             size: int, af: Domain = AF_INET, flags = 0'i32): int {.
             tags: [WriteIOEffect].} =
  ## low-level sendTo proc. This proc sends ``data`` to the specified ``address``,
  ## which may be an IP address or a hostname, if a hostname is specified 
  ## this function will try each IP of that hostname.
  ##
  ## **Note:** This proc is not available for SSL sockets.
  var hints: AddrInfo
  var aiList: ptr AddrInfo = nil
  hints.ai_family = toInt(af)
  hints.ai_socktype = toInt(SOCK_STREAM)
  hints.ai_protocol = toInt(IPPROTO_TCP)
  gaiNim(address, port, hints, aiList)
  
  # try all possibilities:
  var success = false
  var it = aiList
  while it != nil:
    result = sendto(socket.fd, data, size.cint, flags.cint, it.ai_addr,
                    it.ai_addrlen.SockLen)
    if result != -1'i32:
      success = true
      break
    it = it.ai_next

  freeaddrinfo(aiList)

proc sendTo*(socket: Socket, address: string, port: Port, 
             data: string): int {.tags: [WriteIOEffect].} =
  ## Friendlier version of the low-level ``sendTo``.
  result = socket.sendTo(address, port, cstring(data), data.len)

when defined(Windows):
  const
    IOCPARM_MASK = 127
    IOC_IN = int(-2147483648)
    FIONBIO = IOC_IN.int32 or ((sizeof(int32) and IOCPARM_MASK) shl 16) or 
                             (102 shl 8) or 126

  proc ioctlsocket(s: SocketHandle, cmd: clong, 
                   argptr: ptr clong): cint {.
                   stdcall, importc:"ioctlsocket", dynlib: "ws2_32.dll".}

proc setBlocking(s: Socket, blocking: bool) =
  when defined(Windows):
    var mode = clong(ord(not blocking)) # 1 for non-blocking, 0 for blocking
    if ioctlsocket(s.fd, FIONBIO, addr(mode)) == -1:
      raiseOSError(osLastError())
  else: # BSD sockets
    var x: int = fcntl(s.fd, F_GETFL, 0)
    if x == -1:
      raiseOSError(osLastError())
    else:
      var mode = if blocking: x and not O_NONBLOCK else: x or O_NONBLOCK
      if fcntl(s.fd, F_SETFL, mode) == -1:
        raiseOSError(osLastError())
  s.nonblocking = not blocking

discard """ proc setReuseAddr*(s: Socket) =
  var blah: int = 1
  var mode = SO_REUSEADDR
  if setsockopt(s.fd, SOL_SOCKET, mode, addr blah, TSOcklen(sizeof(int))) == -1:
    raiseOSError(osLastError()) """

proc connect*(socket: Socket, address: string, port = Port(0), timeout: int,
             af: Domain = AF_INET) {.tags: [ReadIOEffect, WriteIOEffect].} =
  ## Connects to server as specified by ``address`` on port specified by ``port``.
  ##
  ## The ``timeout`` paremeter specifies the time in miliseconds to allow for
  ## the connection to the server to be made.
  let originalStatus = not socket.nonblocking
  socket.setBlocking(false)
  
  socket.connectAsync(address, port, af)
  var s: seq[Socket] = @[socket]
  if selectWrite(s, timeout) != 1:
    raise newException(TimeoutError, "Call to 'connect' timed out.")
  else:
    when defined(ssl):
      if socket.isSSL:
        socket.setBlocking(true)
        doAssert socket.handshake()
  socket.setBlocking(originalStatus)

proc isSSL*(socket: Socket): bool = return socket.isSSL
  ## Determines whether ``socket`` is a SSL socket.

proc getFD*(socket: Socket): SocketHandle = return socket.fd
  ## Returns the socket's file descriptor

proc isBlocking*(socket: Socket): bool = not socket.nonblocking
  ## Determines whether ``socket`` is blocking.

when defined(Windows):
  var wsa: WSAData
  if wsaStartup(0x0101'i16, addr wsa) != 0: raiseOSError(osLastError())


