#
#                     Chronos
#
#           (c) Copyright 2015 Dominik Picheta
#  (c) Copyright 2018-Present Status Research & Development GmbH
#
#                Licensed under either of
#    Apache License, version 2.0, (LICENSE-APACHEv2)
#                MIT license (LICENSE-MIT)

import os, tables, strutils, times, heapqueue, options, deques, cstrutils
import srcloc
export srcloc

const
  LocCreateIndex = 0
  LocCompleteIndex = 1

type
  CallbackFunc* = proc (arg: pointer = nil) {.gcsafe.}
  CallSoonProc* = proc (c: CallbackFunc, u: pointer = nil) {.gcsafe.}

  AsyncCallback* = object
    function*: CallbackFunc
    udata*: pointer
    deleted*: bool

  # ZAH: This can probably be stored with a cheaper representation
  # until the moment it needs to be printed to the screen (e.g. seq[StackTraceEntry])
  StackTrace = string

  FutureBase* = ref object of RootObj ## Untyped future.
    location: array[2, ptr SrcLoc]
    callbacks: Deque[AsyncCallback]
    finished: bool
    error*: ref Exception ## Stored exception
    errorStackTrace*: StackTrace
    stackTrace: StackTrace ## For debugging purposes only.
    id: int

  # ZAH: we have discussed some possible optimizations where
  # the future can be stored within the caller's stack frame.
  # How much refactoring is needed to make this a regular non-ref type?
  # Obviously, it will still be allocated on the heap when necessary.
  Future*[T] = ref object of FutureBase ## Typed future.
    value: T ## Stored value

  FutureStr*[T] = ref object of Future[T]
    ## Future to hold GC strings
    gcholder*: string

  FutureSeq*[A, B] = ref object of Future[A]
    ## Future to hold GC seqs
    gcholder*: seq[B]

  FutureVar*[T] = distinct Future[T]

  FutureError* = object of Exception
    cause*: FutureBase

var currentID* {.threadvar.}: int
currentID = 0

# ZAH: This seems unnecessary. Isn't it easy to introduce a seperate
# module for the dispatcher type, so it can be directly referenced here?
var callSoonHolder {.threadvar.}: CallSoonProc

proc getCallSoonProc*(): CallSoonProc {.gcsafe.} =
  ## Get current implementation of ``callSoon``.
  return callSoonHolder

proc setCallSoonProc*(p: CallSoonProc) =
  ## Change current implementation of ``callSoon``.
  callSoonHolder = p

proc callSoon*(c: CallbackFunc, u: pointer = nil) =
  ## Call ``cbproc`` "soon".
  callSoonHolder(c, u)

template setupFutureBase(loc: ptr SrcLoc) =
  new(result)
  result.finished = false
  result.stackTrace = getStackTrace()
  result.id = currentID
  result.location[LocCreateIndex] = loc
  currentID.inc()

## ZAH: As far as I undestand `fromProc` is just a debugging helper.
## It would be more efficient if it's represented as a simple statically
## known `char *` in the final program (so it needs to be a `cstring` in Nim).
## The public API can be defined as a template expecting a `static[string]`
## and converting this immediately to a `cstring`.
proc newFuture[T](loc: ptr SrcLoc): Future[T] =
  setupFutureBase(loc)

proc newFutureSeq[A, B](loc: ptr SrcLoc): FutureSeq[A, B] =
  setupFutureBase(loc)

proc newFutureStr[T](loc: ptr SrcLoc): FutureStr[T] =
  setupFutureBase(loc)

proc newFutureVar[T](loc: ptr SrcLoc): FutureVar[T] =
  FutureVar[T](newFuture[T](loc))

template newFuture*[T](fromProc: static[string] = ""): auto =
  ## Creates a new future.
  ##
  ## Specifying ``fromProc``, which is a string specifying the name of the proc
  ## that this future belongs to, is a good habit as it helps with debugging.
  newFuture[T](getSrcLocation(fromProc))

template newFutureSeq*[A, B](fromProc: static[string] = ""): auto =
  ## Create a new future which can hold/preserve GC sequence until future will
  ## not be completed.
  ##
  ## Specifying ``fromProc``, which is a string specifying the name of the proc
  ## that this future belongs to, is a good habit as it helps with debugging.
  newFutureSeq[A, B](getSrcLocation(fromProc))

template newFutureStr*[T](fromProc: static[string] = ""): auto =
  ## Create a new future which can hold/preserve GC string until future will
  ## not be completed.
  ##
  ## Specifying ``fromProc``, which is a string specifying the name of the proc
  ## that this future belongs to, is a good habit as it helps with debugging.
  newFutureStr[T](getSrcLocation(fromProc))

template newFutureVar*[T](fromProc: static[string] = ""): auto =
  ## Create a new ``FutureVar``. This Future type is ideally suited for
  ## situations where you want to avoid unnecessary allocations of Futures.
  ##
  ## Specifying ``fromProc``, which is a string specifying the name of the proc
  ## that this future belongs to, is a good habit as it helps with debugging.
  newFutureVar[T](getSrcLocation(fromProc))

proc clean*[T](future: FutureVar[T]) =
  ## Resets the ``finished`` status of ``future``.
  Future[T](future).finished = false
  Future[T](future).error = nil

proc checkFinished[T](future: Future[T], loc: ptr SrcLoc) =
  ## Checks whether `future` is finished. If it is then raises a
  ## ``FutureError``.
  if future.finished:
    var msg = ""
    msg.add("An attempt was made to complete a Future more than once. ")
    msg.add("Details:")
    msg.add("\n  Future ID: " & $future.id)
    msg.add("\n  Creation location:")
    msg.add("\n    " & $future.location[LocCreateIndex])
    msg.add("\n  First completion location:")
    msg.add("\n    " & $future.location[LocCompleteIndex])
    msg.add("\n  Second completion location:")
    msg.add("\n    " & $loc)
    msg.add("\n  Stack trace to moment of creation:")
    msg.add("\n" & indent(future.stackTrace.strip(), 4))
    when T is string:
      msg.add("\n  Contents (string): ")
      msg.add("\n" & indent(future.value.repr, 4))
    msg.add("\n  Stack trace to moment of secondary completion:")
    msg.add("\n" & indent(getStackTrace().strip(), 4))
    msg.add("\n\n")
    var err = newException(FutureError, msg)
    err.cause = future
    raise err
  else:
    future.location[LocCompleteIndex] = loc

proc call(callbacks: var Deque[AsyncCallback]) =
  var count = len(callbacks)
  while count > 0:
    var item = callbacks.popFirst()
    if not item.deleted:
      callSoon(item.function, item.udata)
    dec(count)

proc add(callbacks: var Deque[AsyncCallback], item: AsyncCallback) =
  if len(callbacks) == 0:
    callbacks = initDeque[AsyncCallback]()
  callbacks.addLast(item)

proc remove(callbacks: var Deque[AsyncCallback], item: AsyncCallback) =
  for p in callbacks.mitems():
    if p.function == item.function and p.udata == item.udata:
      p.deleted = true

proc complete[T](future: Future[T], val: T, loc: ptr SrcLoc) =
  checkFinished(future, loc)
  doAssert(isNil(future.error))
  future.value = val
  future.finished = true
  future.callbacks.call()

template complete*[T](future: Future[T], val: T) =
  ## Completes ``future`` with value ``val``.
  complete(future, val, getSrcLocation())

proc complete(future: Future[void], loc: ptr SrcLoc) =
  ## Completes a void ``future``.
  checkFinished(future, loc)
  doAssert(isNil(future.error))
  future.finished = true
  future.callbacks.call()

template complete*(future: Future[void]) =
  complete(future, getSrcLocation())

proc complete[T](future: FutureVar[T], loc: ptr SrcLoc) =
  template fut: untyped = Future[T](future)
  checkFinished(fut, loc)
  doAssert(isNil(fut.error))
  fut.finished = true
  fut.callbacks.call()

template complete*[T](futvar: FutureVar[T]) =
  ## Completes a ``FutureVar``.
  complete(futvar, getSrcLocation())

proc complete[T](futvar: FutureVar[T], val: T, loc: ptr SrcLoc) =
  template fut: untyped = Future[T](futvar)
  checkFinished(fut, loc)
  doAssert(isNil(fut.error))
  fut.finished = true
  fut.value = val
  fut.callbacks.call()

template complete*[T](futvar: FutureVar[T], val: T) =
  ## Completes a ``FutureVar`` with value ``val``.
  ##
  ## Any previously stored value will be overwritten.
  complete(futvar, val, getSrcLocation())

proc fail[T](future: Future[T], error: ref Exception, loc: ptr SrcLoc) =
  checkFinished(future, loc)
  future.finished = true
  future.error = error
  future.errorStackTrace =
    if getStackTrace(error) == "": getStackTrace() else: getStackTrace(error)
  future.callbacks.call()

template fail*[T](future: Future[T], error: ref Exception) =
  ## Completes ``future`` with ``error``.
  fail(future, error, getSrcLocation())

proc clearCallbacks(future: FutureBase) =
  var count = len(future.callbacks)
  while count > 0:
    discard future.callbacks.popFirst()
    dec(count)

proc addCallback*(future: FutureBase, cb: CallbackFunc, udata: pointer = nil) =
  ## Adds the callbacks proc to be called when the future completes.
  ##
  ## If future has already completed then ``cb`` will be called immediately.
  doAssert(not isNil(cb))
  if future.finished:
    # ZAH: it seems that the Future needs to know its associated Dispatcher
    callSoon(cb, udata)
  else:
    let acb = AsyncCallback(function: cb, udata: udata)
    future.callbacks.add acb

proc addCallback*[T](future: Future[T], cb: CallbackFunc) =
  ## Adds the callbacks proc to be called when the future completes.
  ##
  ## If future has already completed then ``cb`` will be called immediately.
  future.addCallback(cb, cast[pointer](future))

proc removeCallback*(future: FutureBase, cb: CallbackFunc,
                     udata: pointer = nil) =
  doAssert(not isNil(cb))
  let acb = AsyncCallback(function: cb, udata: udata)
  future.callbacks.remove acb

proc removeCallback*[T](future: Future[T], cb: CallbackFunc) =
  future.removeCallback(cb, cast[pointer](future))

proc `callback=`*(future: FutureBase, cb: CallbackFunc, udata: pointer = nil) =
  ## Clears the list of callbacks and sets the callback proc to be called when
  ## the future completes.
  ##
  ## If future has already completed then ``cb`` will be called immediately.
  ##
  ## It's recommended to use ``addCallback`` or ``then`` instead.
  # ZAH: how about `setLen(1); callbacks[0] = cb`
  future.clearCallbacks
  future.addCallback(cb, udata)

proc `callback=`*[T](future: Future[T], cb: CallbackFunc) =
  ## Sets the callback proc to be called when the future completes.
  ##
  ## If future has already completed then ``cb`` will be called immediately.
  `callback=`(future, cb, cast[pointer](future))

proc getHint(entry: StackTraceEntry): string =
  ## We try to provide some hints about stack trace entries that the user
  ## may not be familiar with, in particular calls inside the stdlib.
  result = ""
  if entry.procname == "processPendingCallbacks":
    if cmpIgnoreStyle(entry.filename, "asyncdispatch.nim") == 0:
      return "Executes pending callbacks"
  elif entry.procname == "poll":
    if cmpIgnoreStyle(entry.filename, "asyncdispatch.nim") == 0:
      return "Processes asynchronous completion events"

  if entry.procname.endsWith("_continue"):
    if cmpIgnoreStyle(entry.filename, "asyncmacro.nim") == 0:
      return "Resumes an async procedure"

proc `$`*(entries: seq[StackTraceEntry]): string =
  result = ""
  # Find longest filename & line number combo for alignment purposes.
  var longestLeft = 0
  for entry in entries:
    if isNil(entry.procName): continue

    let left = $entry.filename & $entry.line
    if left.len > longestLeft:
      longestLeft = left.len

  var indent = 2
  # Format the entries.
  for entry in entries:
    if isNil(entry.procName):
      if entry.line == -10:
        result.add(spaces(indent) & "#[\n")
        indent.inc(2)
      else:
        indent.dec(2)
        result.add(spaces(indent) & "]#\n")
      continue

    let left = "$#($#)" % [$entry.filename, $entry.line]
    result.add((spaces(indent) & "$#$# $#\n") % [
      left,
      spaces(longestLeft - left.len + 2),
      $entry.procName
    ])
    let hint = getHint(entry)
    if hint.len > 0:
      result.add(spaces(indent+2) & "## " & hint & "\n")

proc injectStacktrace[T](future: Future[T]) =
  const header = "\nAsync traceback:\n"

  var exceptionMsg = future.error.msg
  if header in exceptionMsg:
    # This is messy: extract the original exception message from the msg
    # containing the async traceback.
    let start = exceptionMsg.find(header)
    exceptionMsg = exceptionMsg[0..<start]

  var newMsg = exceptionMsg & header

  let entries = getStackTraceEntries(future.error)
  newMsg.add($entries)

  newMsg.add("Exception message: " & exceptionMsg & "\n")
  newMsg.add("Exception type:")

  # # For debugging purposes
  # for entry in getStackTraceEntries(future.error):
  #   newMsg.add "\n" & $entry
  future.error.msg = newMsg

proc read*[T](future: Future[T] | FutureVar[T]): T =
  ## Retrieves the value of ``future``. Future must be finished otherwise
  ## this function will fail with a ``ValueError`` exception.
  ##
  ## If the result of the future is an error then that error will be raised.
  {.push hint[ConvFromXtoItselfNotNeeded]: off.}
  let fut = Future[T](future)
  {.pop.}
  if fut.finished:
    if not isNil(fut.error):
      injectStacktrace(fut)
      raise fut.error
    when T isnot void:
      return fut.value
  else:
    # TODO: Make a custom exception type for this?
    raise newException(ValueError, "Future still in progress.")

proc readError*[T](future: Future[T]): ref Exception =
  ## Retrieves the exception stored in ``future``.
  ##
  ## An ``ValueError`` exception will be thrown if no exception exists
  ## in the specified Future.
  if not isNil(future.error): return future.error
  else:
    raise newException(ValueError, "No error in future.")

proc mget*[T](future: FutureVar[T]): var T =
  ## Returns a mutable value stored in ``future``.
  ##
  ## Unlike ``read``, this function will not raise an exception if the
  ## Future has not been finished.
  result = Future[T](future).value

proc finished*(future: FutureBase | FutureVar): bool =
  ## Determines whether ``future`` has completed.
  ##
  ## ``True`` may indicate an error or a value. Use ``failed`` to distinguish.
  when future is FutureVar:
    result = (FutureBase(future)).finished
  else:
    result = future.finished

proc failed*(future: FutureBase): bool =
  ## Determines whether ``future`` completed with an error.
  return (not isNil(future.error))

proc asyncCheck*[T](future: Future[T]) =
  ## Sets a callback on ``future`` which raises an exception if the future
  ## finished with an error.
  ##
  ## This should be used instead of ``discard`` to discard void futures.
  doAssert(not isNil(future), "Future is nil")
  proc cb(data: pointer) =
    if future.failed:
      injectStacktrace(future)
      raise future.error
  future.callback = cb

proc asyncDiscard*[T](future: Future[T]) = discard
  ## This is async workaround for discard ``Future[T]``.

# ZAH: The return type here could be a Future[(T, Y)]
proc `and`*[T, Y](fut1: Future[T], fut2: Future[Y]): Future[void] =
  ## Returns a future which will complete once both ``fut1`` and ``fut2``
  ## complete.
  # ZAH: The Rust implementation of futures is making the case that the
  # `and` combinator can be implemented in a more efficient way without
  # resorting to closures and callbacks. I haven't thought this through
  # completely yet, but here is their write-up:
  # http://aturon.github.io/2016/09/07/futures-design/
  #
  # We should investigate this further, before settling on the final design.
  # The same reasoning applies to `or` and `all`.
  var retFuture = newFuture[void]("asyncdispatch.`and`")
  proc cb(data: pointer) =
    if not retFuture.finished:
      if (fut1.failed or fut1.finished) and (fut2.failed or fut2.finished):
        if cast[pointer](fut1) == data:
          if fut1.failed: retFuture.fail(fut1.error)
          elif fut2.finished: retFuture.complete()
        else:
          if fut2.failed: retFuture.fail(fut2.error)
          elif fut1.finished: retFuture.complete()
  fut1.callback = cb
  fut2.callback = cb
  return retFuture

proc `or`*[T, Y](fut1: Future[T], fut2: Future[Y]): Future[void] =
  ## Returns a future which will complete once either ``fut1`` or ``fut2``
  ## complete.
  var retFuture = newFuture[void]("asyncdispatch.`or`")
  proc cb(data: pointer) {.gcsafe.} =
    if not retFuture.finished:
      var fut = cast[FutureBase](data)
      if cast[pointer](fut1) == data:
        fut2.removeCallback(cb)
      else:
        fut1.removeCallback(cb)
      if fut.failed: retFuture.fail(fut.error)
      else: retFuture.complete()
  fut1.callback = cb
  fut2.callback = cb
  return retFuture

proc all*[T](futs: varargs[Future[T]]): auto =
  ## Returns a future which will complete once all futures in ``futs`` complete.
  ## If the argument is empty, the returned future completes immediately.
  ##
  ## If the awaited futures are not ``Future[void]``, the returned future
  ## will hold the values of all awaited futures in a sequence.
  ##
  ## If the awaited futures *are* ``Future[void]``, this proc returns
  ## ``Future[void]``.
  ##
  ## Note, that if one of the futures in ``futs`` will fail, result of ``all()``
  ## will also be failed with error from failed future.
  let totalFutures = len(futs)
  var completedFutures = 0

  # Because we can't capture varargs[T] in closures we need to create copy.
  var nfuts = @futs

  when T is void:
    var retFuture = newFuture[void]("asyncdispatch.all(void)")
    for fut in nfuts:
      fut.addCallback proc (data: pointer) =
        inc(completedFutures)
        if not retFuture.finished:
          if completedFutures == totalFutures:
            for nfut in nfuts:
              if nfut.failed:
                retFuture.fail(nfut.error)
                break
            if not retFuture.failed:
              retFuture.complete()

    if len(nfuts) == 0:
      retFuture.complete()

    return retFuture
  else:
    var retFuture = newFuture[seq[T]]("asyncdispatch.all(T)")
    var retValues = newSeq[T](totalFutures)
    for fut in nfuts:
      fut.addCallback proc (data: pointer) =
        inc(completedFutures)
        if not retFuture.finished:
          if completedFutures == totalFutures:
            for k, nfut in nfuts:
              if nfut.failed:
                retFuture.fail(nfut.error)
                break
              else:
                retValues[k] = nfut.read()
            if not retFuture.failed:
              retFuture.complete(retValues)

    if len(nfuts) == 0:
      retFuture.complete(retValues)

    return retFuture