stdin.pony

use @pony_asio_event_create[AsioEventID](
  owner: AsioEventNotify,
  fd: U32,
  flags: U32,
  nsec: U64,
  noisy: Bool)

use @pony_asio_event_unsubscribe[None](event: AsioEventID)
use @pony_asio_event_destroy[None](event: AsioEventID)

interface InputNotify
  """
  Notification for data arriving via an input stream.
  """
  fun ref apply(data: Array[U8] iso) =>
    """
    Called when data is available on the stream.
    """
    None

  fun ref dispose() =>
    """
    Called when no more data will arrive on the stream.
    """
    None

interface tag DisposableActor
  """
  An interface used to asynchronously dispose of an actor.
  """
  be dispose()

interface tag InputStream
  """
  Asynchronous access to some input stream.
  """
  be apply(notify: (InputNotify iso | None), chunk_size: USize = 32)
    """
    Set the notifier. Optionally, also sets the chunk size, dictating the
    maximum number of bytes of each chunk that will be passed to the notifier.
    """

  be dispose() =>
    """
    Clear the notifier in order to shut down input.
    """
    None

actor Stdin
  """
  Asynchronous access to stdin. The constructor is private to ensure that
  access is provided only via an environment.

  Reading from stdin is done by registering an `InputNotify`:

  ```pony
  actor Main
    new create(env: Env) =>
      // do not forget to call `env.input.dispose` at some point
      env.input(
        object iso is InputNotify
          fun ref apply(data: Array[U8] iso) =>
            env.out.write(String.from_iso_array(consume data))

          fun ref dispose() =>
            env.out.print("Done.")
        end,
        512)
  ```

  **Note:** For reading user input from a terminal, use the [term](term--index) package.
  """
  var _notify: (InputNotify | None) = None
  var _chunk_size: USize = 32
  var _event: AsioEventID = AsioEvent.none()
  let _use_event: Bool

  new _create(use_event: Bool) =>
    """
    Create an asynchronous stdin provider.
    """
    _use_event = use_event

  be apply(notify: (InputNotify iso | None), chunk_size: USize = 32) =>
    """
    Set the notifier. Optionally, also sets the chunk size, dictating the
    maximum number of bytes of each chunk that will be passed to the notifier.
    """
    _set_notify(consume notify)
    _chunk_size = chunk_size

  be dispose() =>
    """
    Clear the notifier in order to shut down input.
    """
    _set_notify(None)

  fun ref _set_notify(notify: (InputNotify iso | None)) =>
    """
    Set the notifier.
    """
    if notify is None then
      if _use_event and not _event.is_null() then
        // Unsubscribe the event.
        @pony_asio_event_unsubscribe(_event)
        _event = AsioEvent.none()
      end
    elseif _notify is None then
      if _use_event then
        // Create a new event.
        _event = @pony_asio_event_create(this, 0, AsioEvent.read(), 0, true)
      else
        // Start the read loop.
        _loop_read()
      end
    end

    try (_notify as InputNotify).dispose() end
    _notify = consume notify

  be _loop_read() =>
    """
    If we are able to read from stdin, schedule another read.
    """
    if _read() then
      _loop_read()
    end

  be _event_notify(event: AsioEventID, flags: U32, arg: U32) =>
    """
    When the event fires, read from stdin.
    """
    if AsioEvent.disposable(flags) then
      @pony_asio_event_destroy(event)
    elseif (_event is event) and AsioEvent.readable(flags) then
      _read()
    end

  be _read_again() =>
    """
    Resume reading.
    """
    _read()

  fun ref _read(): Bool =>
    """
    Read a chunk of data from stdin. Read a maximum of _chunk_size bytes, send
    ourself a resume message and stop reading to avoid starving other actors.
    """
    try
      let notify = _notify as InputNotify
      var sum: USize = 0

      while true do
        let chunk_size = _chunk_size
        var data = recover Array[U8] .> undefined(chunk_size) end
        var again: Bool = false

        let len =
          @pony_os_stdin_read[USize](data.cpointer(), data.space(),
            addressof again)

        match len
        | -1 =>
          // Error, possibly would block. Try again.
          return true
        | 0 =>
          // EOF. Close everything, stop reading.
          _close_event()
          notify.dispose()
          _notify = None
          return false
        end

        data.truncate(len)
        notify(consume data)

        if not again then
          // Not allowed to call pony_os_stdin_read again yet, exit loop.
          return true
        end

        sum = sum + len

        if sum > (1 << 12) then
          if _use_event then
            _read_again()
          end

          break
        end
      end
      true
    else
      // No notifier. Stop reading.
      _close_event()
      false
    end

    fun ref _close_event() =>
      """
      Close the event.
      """
      if not _event.is_null() then
        @pony_asio_event_unsubscribe(_event)
        _event = AsioEvent.none()
      end