discard """
  file: "twinasyncrw.nim"
  output: "5000"
"""
when defined(windows):
  import asyncdispatch, nativesockets, net, strutils, os, winlean

  var msgCount = 0

  const
    swarmSize = 50
    messagesToSend = 100

  var clientCount = 0

  proc winConnect*(socket: AsyncFD, address: string, port: Port,
    domain = Domain.AF_INET): Future[void] =
    var retFuture = newFuture[void]("winConnect")
    proc cb(fd: AsyncFD): bool =
      var ret = SocketHandle(fd).getSockOptInt(cint(SOL_SOCKET), cint(SO_ERROR))
      if ret == 0:
          # We have connected.
          retFuture.complete()
          return true
      else:
          retFuture.fail(newException(OSError, osErrorMsg(OSErrorCode(ret))))
          return true

    var aiList = getAddrInfo(address, port, domain)
    var success = false
    var lastError: OSErrorCode = OSErrorCode(0)
    var it = aiList
    while it != nil:
      var ret = nativesockets.connect(socket.SocketHandle, it.ai_addr, it.ai_addrlen.Socklen)
      if ret == 0:
        # Request to connect completed immediately.
        success = true
        retFuture.complete()
        break
      else:
        lastError = osLastError()
        if lastError.int32 == WSAEWOULDBLOCK:
          success = true
          addWrite(socket, cb)
          break
        else:
          success = false
      it = it.ai_next

    dealloc(aiList)
    if not success:
      retFuture.fail(newException(OSError, osErrorMsg(lastError)))
    return retFuture

  proc winRecv*(socket: AsyncFD, size: int,
             flags = {SocketFlag.SafeDisconn}): Future[string] =
    var retFuture = newFuture[string]("recv")

    var readBuffer = newString(size)

    proc cb(sock: AsyncFD): bool =
      result = true
      let res = recv(sock.SocketHandle, addr readBuffer[0], size.cint,
                     flags.toOSFlags())
      if res < 0:
        let lastError = osLastError()
        if flags.isDisconnectionError(lastError):
          retFuture.complete("")
        else:
          retFuture.fail(newException(OSError, osErrorMsg(lastError)))
      elif res == 0:
        # Disconnected
        retFuture.complete("")
      else:
        readBuffer.setLen(res)
        retFuture.complete(readBuffer)
    # TODO: The following causes a massive slowdown.
    #if not cb(socket):
    addRead(socket, cb)
    return retFuture

  proc winRecvInto*(socket: AsyncFD, buf: cstring, size: int,
                  flags = {SocketFlag.SafeDisconn}): Future[int] =
    var retFuture = newFuture[int]("winRecvInto")

    proc cb(sock: AsyncFD): bool =
      result = true
      let res = nativesockets.recv(sock.SocketHandle, buf, size.cint,
                                   flags.toOSFlags())
      if res < 0:
        let lastError = osLastError()
        if flags.isDisconnectionError(lastError):
          retFuture.complete(0)
        else:
          retFuture.fail(newException(OSError, osErrorMsg(lastError)))
      else:
        retFuture.complete(res)
    # TODO: The following causes a massive slowdown.
    #if not cb(socket):
    addRead(socket, cb)
    return retFuture

  proc winSend*(socket: AsyncFD, data: string,
             flags = {SocketFlag.SafeDisconn}): Future[void] =
    var retFuture = newFuture[void]("winSend")

    var written = 0

    proc cb(sock: AsyncFD): bool =
      result = true
      let netSize = data.len-written
      var d = data.cstring
      let res = nativesockets.send(sock.SocketHandle, addr d[written], netSize.cint, 0)
      if res < 0:
        let lastError = osLastError()
        if flags.isDisconnectionError(lastError):
          retFuture.complete()
        else:
          retFuture.fail(newException(OSError, osErrorMsg(lastError)))
      else:
        written.inc(res)
        if res != netSize:
          result = false # We still have data to send.
        else:
          retFuture.complete()
    # TODO: The following causes crashes.
    #if not cb(socket):
    addWrite(socket, cb)
    return retFuture

  proc winAcceptAddr*(socket: AsyncFD, flags = {SocketFlag.SafeDisconn}):
      Future[tuple[address: string, client: AsyncFD]] =
    var retFuture = newFuture[tuple[address: string,
        client: AsyncFD]]("winAcceptAddr")
    proc cb(sock: AsyncFD): bool =
      result = true
      if not retFuture.finished:
        var sockAddress = Sockaddr()
        var addrLen = sizeof(sockAddress).Socklen
        var client = nativesockets.accept(sock.SocketHandle,
                                          cast[ptr SockAddr](addr(sockAddress)), addr(addrLen))
        if client == osInvalidSocket:
          retFuture.fail(newException(OSError, osErrorMsg(osLastError())))
        else:
          retFuture.complete((getAddrString(cast[ptr SockAddr](addr sockAddress)), client.AsyncFD))

    addRead(socket, cb)
    return retFuture

  proc winAccept*(socket: AsyncFD,
      flags = {SocketFlag.SafeDisconn}): Future[AsyncFD] =
    ## Accepts a new connection. Returns a future containing the client socket
    ## corresponding to that connection.
    ## The future will complete when the connection is successfully accepted.
    var retFut = newFuture[AsyncFD]("winAccept")
    var fut = winAcceptAddr(socket, flags)
    fut.callback =
      proc (future: Future[tuple[address: string, client: AsyncFD]]) =
        assert future.finished
        if future.failed:
          retFut.fail(future.error)
        else:
          retFut.complete(future.read.client)
    return retFut


  proc winRecvLine*(socket: AsyncFD): Future[string] {.async.} =
    ## Reads a line of data from ``socket``. Returned future will complete once
    ## a full line is read or an error occurs.
    ##
    ## 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 ``""``.
    ##
    ## If the socket is disconnected in the middle of a line (before ``\r\L``
    ## is read) then line will be set to ``""``.
    ## The partial line **will be lost**.
    ##
    ## **Warning**: This assumes that lines are delimited by ``\r\L``.
    ##
    ## **Note**: This procedure is mostly used for testing. You likely want to
    ## use ``asyncnet.recvLine`` instead.

    template addNLIfEmpty(): stmt =
      if result.len == 0:
        result.add("\c\L")

    result = ""
    var c = ""
    while true:
      c = await winRecv(socket, 1)
      if c.len == 0:
        return ""
      if c == "\r":
        c = await winRecv(socket, 1)
        assert c == "\l"
        addNLIfEmpty()
        return
      elif c == "\L":
        addNLIfEmpty()
        return
      add(result, c)

  proc sendMessages(client: AsyncFD) {.async.} =
    for i in 0 .. <messagesToSend:
      await winSend(client, "Message " & $i & "\c\L")

  proc launchSwarm(port: Port) {.async.} =
    for i in 0 .. <swarmSize:
      var sock = newNativeSocket()
      setBlocking(sock, false)

      await winConnect(AsyncFD(sock), "localhost", port)
      await sendMessages(AsyncFD(sock))
      discard closeSocket(sock)

  proc readMessages(client: AsyncFD) {.async.} =
    while true:
      var line = await winRecvLine(client)
      if line == "":
        closeSocket(client)
        clientCount.inc
        break
      else:
        if line.startswith("Message "):
          msgCount.inc
        else:
          doAssert false

  proc createServer(port: Port) {.async.} =
    var server = newNativeSocket()
    setBlocking(server, false)
    block:
      var name = Sockaddr_in()
      name.sin_family = toInt(Domain.AF_INET).int16
      name.sin_port = htons(uint16(port))
      name.sin_addr.s_addr = htonl(INADDR_ANY)
      if bindAddr(server, cast[ptr SockAddr](addr(name)),
                  sizeof(name).Socklen) < 0'i32:
        raiseOSError(osLastError())

    discard server.listen()
    while true:
      asyncCheck readMessages(await winAccept(AsyncFD(server)))

  asyncCheck createServer(Port(10335))
  asyncCheck launchSwarm(Port(10335))
  while true:
    poll()
    if clientCount == swarmSize: break

  assert msgCount == swarmSize * messagesToSend
  echo msgCount
else:
  echo(5000)
