#
#
#            Nim's Runtime Library
#        (c) Copyright 2016 Eugene Kabanov
#
#    See the file "copying.txt", included in this
#    distribution, for details about the copyright.
#

## This module allows high-level and efficient I/O multiplexing.
##
## Supported OS primitives: ``epoll``, ``kqueue``, ``poll`` and
## Windows ``select``.
##
## To use threadsafe version of this module, it needs to be compiled
## with both ``-d:threadsafe`` and ``--threads:on`` options.
##
## Supported features: files, sockets, pipes, timers, processes, signals
## and user events.
##
## Fully supported OS: MacOSX, FreeBSD, OpenBSD, NetBSD, Linux (except
## for Android).
##
## Partially supported OS: Windows (only sockets and user events),
## Solaris (files, sockets, handles and user events).
## Android (files, sockets, handles and user events).
##
## TODO: ``/dev/poll``, ``event ports`` and filesystem events.

import os, strutils, nativesockets

const hasThreadSupport = compileOption("threads") and defined(threadsafe)

const ioselSupportedPlatform* = defined(macosx) or defined(freebsd) or
                                defined(netbsd) or defined(openbsd) or
                                defined(dragonfly) or
                                (defined(linux) and not defined(android))
  ## This constant is used to determine whether the destination platform is
  ## fully supported by ``ioselectors`` module.

const bsdPlatform = defined(macosx) or defined(freebsd) or
                    defined(netbsd) or defined(openbsd) or
                    defined(dragonfly)

when defined(nimdoc):
  type
    Selector*[T] = ref object
      ## An object which holds descriptors to be checked for read/write status

    Event* {.pure.} = enum
      ## An enum which hold event types
      Read,        ## Descriptor is available for read
      Write,       ## Descriptor is available for write
      Timer,       ## Timer descriptor is completed
      Signal,      ## Signal is raised
      Process,     ## Process is finished
      Vnode,       ## BSD specific file change
      User,        ## User event is raised
      Error,       ## Error occurred while waiting for descriptor
      VnodeWrite,  ## NOTE_WRITE (BSD specific, write to file occurred)
      VnodeDelete, ## NOTE_DELETE (BSD specific, unlink of file occurred)
      VnodeExtend, ## NOTE_EXTEND (BSD specific, file extended)
      VnodeAttrib, ## NOTE_ATTRIB (BSD specific, file attributes changed)
      VnodeLink,   ## NOTE_LINK (BSD specific, file link count changed)
      VnodeRename, ## NOTE_RENAME (BSD specific, file renamed)
      VnodeRevoke  ## NOTE_REVOKE (BSD specific, file revoke occurred)

    ReadyKey* = object
      ## An object which holds result for descriptor
      fd* : int ## file/socket descriptor
      events*: set[Event] ## set of events
      errorCode*: OSErrorCode ## additional error code information for
                              ## Error events

    SelectEvent* = object
      ## An object which holds user defined event

  proc newSelector*[T](): Selector[T] =
    ## Creates a new selector

  proc close*[T](s: Selector[T]) =
    ## Closes the selector.

  proc registerHandle*[T](s: Selector[T], fd: int | SocketHandle,
                          events: set[Event], data: T) =
    ## Registers file/socket descriptor ``fd`` to selector ``s``
    ## with events set in ``events``. The ``data`` is application-defined
    ## data, which will be passed when an event is triggered.

  proc updateHandle*[T](s: Selector[T], fd: int | SocketHandle,
                        events: set[Event]) =
    ## Update file/socket descriptor ``fd``, registered in selector
    ## ``s`` with new events set ``event``.

  proc registerTimer*[T](s: Selector[T], timeout: int, oneshot: bool,
                         data: T): int {.discardable.} =
    ## Registers timer notification with ``timeout`` (in milliseconds)
    ## to selector ``s``.
    ##
    ## If ``oneshot`` is ``true``, timer will be notified only once.
    ##
    ## Set ``oneshot`` to ``false`` if you want periodic notifications.
    ##
    ## The ``data`` is application-defined data, which will be passed, when
    ## the timer is triggered.
    ##
    ## Returns the file descriptor for the registered timer.

  proc registerSignal*[T](s: Selector[T], signal: int,
                          data: T): int {.discardable.} =
    ## Registers Unix signal notification with ``signal`` to selector
    ## ``s``.
    ##
    ## The ``data`` is application-defined data, which will be
    ## passed when signal raises.
    ##
    ## Returns the file descriptor for the registered signal.
    ##
    ## **Note:** This function is not supported on ``Windows``.

  proc registerProcess*[T](s: Selector[T], pid: int,
                           data: T): int {.discardable.} =
    ## Registers a process id (pid) notification (when process has
    ## exited) in selector ``s``.
    ##
    ## The ``data`` is application-defined data, which will be passed when
    ## process with ``pid`` has exited.
    ##
    ## Returns the file descriptor for the registered signal.

  proc registerEvent*[T](s: Selector[T], ev: SelectEvent, data: T) =
    ## Registers selector event ``ev`` in selector ``s``.
    ##
    ## The ``data`` is application-defined data, which will be passed when
    ## ``ev`` happens.

  proc registerVnode*[T](s: Selector[T], fd: cint, events: set[Event],
                         data: T) =
    ## Registers selector BSD/MacOSX specific vnode events for file
    ## descriptor ``fd`` and events ``events``.
    ## ``data`` application-defined data, which to be passed, when
    ## vnode event happens.
    ##
    ## **Note:** This function is supported only by BSD and MacOSX.

  proc newSelectEvent*(): SelectEvent =
    ## Creates a new user-defined event.

  proc trigger*(ev: SelectEvent) =
    ## Trigger event ``ev``.

  proc close*(ev: SelectEvent) =
    ## Closes user-defined event ``ev``.

  proc unregister*[T](s: Selector[T], ev: SelectEvent) =
    ## Unregisters user-defined event ``ev`` from selector ``s``.

  proc unregister*[T](s: Selector[T], fd: int|SocketHandle|cint) =
    ## Unregisters file/socket descriptor ``fd`` from selector ``s``.

  proc selectInto*[T](s: Selector[T], timeout: int,
                      results: var openarray[ReadyKey]): int =
    ## Waits for events registered in selector ``s``.
    ##
    ## The ``timeout`` argument specifies the maximum number of milliseconds
    ## the function will be blocked for if no events are ready. Specifying a
    ## timeout of ``-1`` causes the function to block indefinitely.
    ## All available events will be stored in ``results`` array.
    ##
    ## Returns number of triggered events.

  proc select*[T](s: Selector[T], timeout: int): seq[ReadyKey] =
    ## Waits for events registered in selector ``s``.
    ##
    ## The ``timeout`` argument specifies the maximum number of milliseconds
    ## the function will be blocked for if no events are ready. Specifying a
    ## timeout of ``-1`` causes the function to block indefinitely.
    ##
    ## Returns a list of triggered events.

  proc getData*[T](s: Selector[T], fd: SocketHandle|int): var T =
    ## Retrieves application-defined ``data`` associated with descriptor ``fd``.
    ## If specified descriptor ``fd`` is not registered, empty/default value
    ## will be returned.

  proc setData*[T](s: Selector[T], fd: SocketHandle|int, data: var T): bool =
    ## Associate application-defined ``data`` with descriptor ``fd``.
    ##
    ## Returns ``true``, if data was successfully updated, ``false`` otherwise.

  template isEmpty*[T](s: Selector[T]): bool = # TODO: Why is this a template?
    ## Returns ``true``, if there are no registered events or descriptors
    ## in selector.

  template withData*[T](s: Selector[T], fd: SocketHandle|int, value,
                        body: untyped) =
    ## Retrieves the application-data assigned with descriptor ``fd``
    ## to ``value``. This ``value`` can be modified in the scope of
    ## the ``withData`` call.
    ##
    ## .. code-block:: nim
    ##
    ##   s.withData(fd, value) do:
    ##     # block is executed only if ``fd`` registered in selector ``s``
    ##     value.uid = 1000
    ##

  template withData*[T](s: Selector[T], fd: SocketHandle|int, value,
                        body1, body2: untyped) =
    ## Retrieves the application-data assigned with descriptor ``fd``
    ## to ``value``. This ``value`` can be modified in the scope of
    ## the ``withData`` call.
    ##
    ## .. code-block:: nim
    ##
    ##   s.withData(fd, value) do:
    ##     # block is executed only if ``fd`` registered in selector ``s``.
    ##     value.uid = 1000
    ##   do:
    ##     # block is executed if ``fd`` not registered in selector ``s``.
    ##     raise
    ##

  proc contains*[T](s: Selector[T], fd: SocketHandle|int): bool {.inline.} =
    ## Determines whether selector contains a file descriptor.

  proc getFd*[T](s: Selector[T]): int =
    ## Retrieves the underlying selector's file descriptor.
    ##
    ## For *poll* and *select* selectors ``-1`` is returned.

else:
  when hasThreadSupport:
    import locks

    type
      SharedArray[T] = UncheckedArray[T]

    proc allocSharedArray[T](nsize: int): ptr SharedArray[T] =
      result = cast[ptr SharedArray[T]](allocShared0(sizeof(T) * nsize))

    proc reallocSharedArray[T](sa: ptr SharedArray[T], nsize: int): ptr SharedArray[T] =
      result = cast[ptr SharedArray[T]](reallocShared(sa, sizeof(T) * nsize))

    proc deallocSharedArray[T](sa: ptr SharedArray[T]) =
      deallocShared(cast[pointer](sa))
  type
    Event* {.pure.} = enum
      Read, Write, Timer, Signal, Process, Vnode, User, Error, Oneshot,
      Finished, VnodeWrite, VnodeDelete, VnodeExtend, VnodeAttrib, VnodeLink,
      VnodeRename, VnodeRevoke

  type
    IOSelectorsException* = object of Exception

    ReadyKey* = object
      fd* : int
      events*: set[Event]
      errorCode*: OSErrorCode

    SelectorKey[T] = object
      ident: int
      events: set[Event]
      param: int
      data: T

  const
    InvalidIdent = -1

  proc raiseIOSelectorsError[T](message: T) =
    var msg = ""
    when T is string:
      msg.add(message)
    elif T is OSErrorCode:
      msg.add(osErrorMsg(message) & " (code: " & $int(message) & ")")
    else:
      msg.add("Internal Error\n")
    var err = newException(IOSelectorsException, msg)
    raise err

  proc setNonBlocking(fd: cint) {.inline.} =
    setBlocking(fd.SocketHandle, false)

  when not defined(windows):
    import posix

    template setKey(s, pident, pevents, pparam, pdata: untyped) =
      var skey = addr(s.fds[pident])
      skey.ident = pident
      skey.events = pevents
      skey.param = pparam
      skey.data = data

  when ioselSupportedPlatform:
    template blockSignals(newmask: var Sigset, oldmask: var Sigset) =
      when hasThreadSupport:
        if posix.pthread_sigmask(SIG_BLOCK, newmask, oldmask) == -1:
          raiseIOSelectorsError(osLastError())
      else:
        if posix.sigprocmask(SIG_BLOCK, newmask, oldmask) == -1:
          raiseIOSelectorsError(osLastError())

    template unblockSignals(newmask: var Sigset, oldmask: var Sigset) =
      when hasThreadSupport:
        if posix.pthread_sigmask(SIG_UNBLOCK, newmask, oldmask) == -1:
          raiseIOSelectorsError(osLastError())
      else:
        if posix.sigprocmask(SIG_UNBLOCK, newmask, oldmask) == -1:
          raiseIOSelectorsError(osLastError())

  template clearKey[T](key: ptr SelectorKey[T]) =
    var empty: T
    key.ident = InvalidIdent
    key.events = {}
    key.data = empty

  proc verifySelectParams(timeout: int) =
    # Timeout of -1 means: wait forever
    # Anything higher is the time to wait in milliseconds.
    doAssert(timeout >= -1, "Cannot select with a negative value, got " & $timeout)

  when defined(linux):
    include ioselects/ioselectors_epoll
  elif bsdPlatform:
    include ioselects/ioselectors_kqueue
  elif defined(windows):
    include ioselects/ioselectors_select
  elif defined(solaris):
    include ioselects/ioselectors_poll # need to replace it with event ports
  elif defined(genode):
    include ioselects/ioselectors_select # TODO: use the native VFS layer
  elif defined(nintendoswitch):
    include ioselects/ioselectors_select
  else:
    include ioselects/ioselectors_poll

proc register*[T](s: Selector[T], fd: int | SocketHandle,
                  events: set[Event], data: T) {.deprecated: "use registerHandle instead".} =
  ## **Deprecated since v0.18.0:** Use ``registerHandle`` instead.
  s.registerHandle(fd, events, data)

proc setEvent*(ev: SelectEvent) {.deprecated: "use trigger instead".} =
  ## Trigger event ``ev``.
  ##
  ## **Deprecated since v0.18.0:** Use ``trigger`` instead.
  ev.trigger()

proc update*[T](s: Selector[T], fd: int | SocketHandle,
                events: set[Event]) {.deprecated: "use updateHandle instead".} =
  ## Update file/socket descriptor ``fd``, registered in selector
  ## ``s`` with new events set ``event``.
  ##
  ## **Deprecated since v0.18.0:** Use ``updateHandle`` instead.
  s.updateHandle()
