-- Light Unix I/O for Lua
-- Copyright 2012 Daniel Silverstone <dsilvers@digital-scurf.org>
--
-- Distributed under the same terms as Lua itself (MIT).
--
-- A low-ish level event-based main-loop for luxio-based programs
-------------------------------------------------------------------------------
--
-- ev = require "luxio.event"
--
-- ev.register_fd(fd, ctx, input_available, output_space, error_event)
--    Register a FD with the poll loop
--                fd is the FD to be registered (should be a FD number)
--                ctx is the context to be passed to the callbacks
--                input_available is the callback for when POLLIN is active
--                                want_more = input_available(ctx, fd, urgent)
--                                urgent is true if POLLPRI was set.
--                output_space is the callback for when POLLOUT is active
--                                have_more = output_space(ctx, fd)
--                error_event is the callback for when POLLERR, POLLNVAL,
--                            or POLLHUP happens.
--                                error_event(ctx, fd, "hup" or "nval" or "err")
--
--    If the callback returns true then the event is re-armed, otherwise
--    you will need to call ev.set_events() later to re-arm the event.
--    The error event cannot be disarmed and is the only event armed
--    when you register a FD.  If the error event is called then the event
--    system will unregister the FD on return from the handler if the handler
--    has not unregistered it itself.
--
-- ev.unregister_fd(fd)
--    Removes a FD from the poll loop
--
-- ev.set_events(fd, input_enabled, output_enabled)
--    Arm/Disarm events for fd.
--               fd is the FD registered with ev.register_fd
--               input_enabled is whether to arm the input event (POLLIN)
--               output_enabled is whether to arm the output event (POLLOUT)
--    If the respective enable is true the event is armed, if it is
--    false then it's disarmed.  If you pass nil for the enable then the
--    current arm/disarm state is unchanged.
--
-- res = ev.step(timeout)
--    Runs one step of the poll loop, returning after timeout milliseconds
--    if nothing happened.
--               timeout is the number of milliseconds to run for.
--                       -1 means 'forever'
--    Returns the number of FDs whose events fired, or 0 if timed out.
--
-- handle = ev.timer(timeout, timer_event, ctx)
--    Call timer_event(ctx) after timeout milliseconds.
--    if the event returns true, re-arms the timer automatically.
--
-- handle = ev.alarm(wallclock, timer_event, ctx)
--    Call timer_event at (or soon after) wallclock (expressed as time_t)
--    If the event returns a number, treat that as a time_t and
--    re-arm the alarm.
--
-- ev.cancel_timer(handle)
--    Cancel the timer or alarm associated with the provided handle.
--
-- ... = ev.run()
--    Runs the poll loop until it is terminated by ev.quit().
--    Returns the values passed to ev.quit()
--
-- ev.quit(...)
--    Causes ev.run() to return to the caller, with the given values as
--    the return values.  Note that this makes no sense unless you use
--    the ev.run() wrapper.
--
-- The event system runs all event callbacks inside a pcall but ignored errors.

local l = require "luxio"

local pollfds_new = l.pollfds_new
local pollfds_resize = l.pollfds_resize
local pollfds_getslot = l.pollfds_getslot
local pollfds_setslot = l.pollfds_setslot

local bor = l.bit.bor
local bclear = l.bit.bclear
local btest = l.bit.btest

local poll = l.poll
local strerror = l.strerror

local pfds = pollfds_new()
local extend_by = 10
local gaps = {}
local fds = {}

local POLLIN = l.POLLIN
local POLLOUT = l.POLLOUT
local POLLPRI = l.POLLPRI
local POLLERR = l.POLLERR
local POLLHUP = l.POLLHUP
local POLLNVAL = l.POLLNVAL
local input_bits = bor(POLLIN, POLLPRI)

local zero_timeval = l.zero_timeval
local gettimeofday = l.gettimeofday

l = nil

local quitvals
local quit_error = {}

local _nval = "nval"
local _hup = "hup"
local _err = "err"

local function _claim_free_slot()
   -- Find a free slot in the pollfds
   local gap = next(gaps)
   if gap then
      gaps[gap] = nil
      return gap
   end
   -- No gaps in the pollfds, so extend
   pollfds_resize(pfds, #pfds + extend_by)
   -- Store the new gaps for later
   for n = #pfds - (extend_by - 2), #pfds do
      gaps[n] = true
   end
   -- And return the one gap we didn't store
   return #pfds - (extend_by - 1)
end

local function _set_events(_fd, input_enable, output_enable)
   local fd = _fd
   if type(_fd) == "table" then
      fd = _fd.fd
   end
   local fdt = fds[fd]
   assert(fdt, "File descriptor '" .. tostring(_fd) .. "' is not registered")

   local events = fdt.events
   
   if input_enable then
      events = bor(events, input_bits)
   elseif input_enable == false then
      events = bclear(events, input_bits)
   end
   if output_enable then
      events = bor(events, POLLOUT)
   elseif output_enable == false then
      events = bclear(events, POLLOUT)
   end
   pollfds_setslot(pfds, fds[fd].slotnr, fd, events, 0)
   fdt.events = events
end

local function _null_evt() end
local function _register_fd(_fd, ctx, input_evt, output_evt, error_evt)
   local fd = _fd
   if type(_fd) == "table" then
      fd = _fd.fd
   end
   assert(fds[fd] == nil, "File descriptor '" .. tostring(_fd) .. "' is already registered")
   local fdt = {
      fd = fd,
      _fd = _fd,
      ctx = ctx,
      input_evt = input_evt or _null_evt,
      output_evt = output_evt or _null_evt,
      error_evt = error_evt or _null_evt,
      slotnr = _claim_free_slot(),
      events = 0
   }
   fds[fd] = fdt
   _set_events(_fd)
end

local function rpcall(...)
   local t = {pcall(...)}
   if not t[1] then
      print("Error during event handling:", t[2])
   end
   return unpack(t)
end
-- Uncomment this to disable error reporting
-- rpcall = pcall

local function _unregister_fd(_fd)
   if type(_fd) == "table" then
      fd = _fd.fd
   end
   local fdt = fds[fd]
   assert(fdt, "File descriptor '" .. tostring(_fd) .. "' is not registered")
   -- pull this fd out of poll
   pollfds_setslot(pfds, fdt.slotnr, -1, 0, 0)
   -- hand the slot back to the gap tracker
   gaps[fdt.slotnr] = true
   -- and unregister the fd from the list
   fds[fd] = nil
end

local function _step(timeout)
   local nfds, err = poll(pfds, timeout)
   if nfds == -1 then
      error(strerror(err))
   end
   local fds_to_process = nfds
   for slot = 1, #pfds do
      if fds_to_process == 0 then
	 break
      end
      local fd, ev, rev = pollfds_getslot(pfds, slot)
      local fdt = fds[fd]
      local cev = 0
      if fd >= 0 and rev ~= 0 then
	 -- Deal with errors first
	 local did_error = false
	 if btest(rev, POLLNVAL) then
	    -- fd is invalid, let the caller know
	    did_error = true
	    rpcall(fdt.error_evt, fdt.ctx, fd, _nval)
	 elseif btest(rev, POLLERR) then
	    -- fd errored
	    did_error = true
	    rpcall(fdt.error_evt, fdt.ctx, fd, _err)
	 elseif btest(rev, POLLHUP) then
	    -- fd HUPped
	    did_error = true
	    rpcall(fdt.error_evt, fdt.ctx, fd, _hup)
	 end
	 if did_error then
	    if fds[fd] then
	       _unregister_fd(fd)
	    end
	 else
	    -- No error, process incoming data first.
	    if btest(rev, POLLPRI) then
	       local ok, want = rpcall(fdt.input_evt, fdt.ctx, fdt._fd, true)
	       if ok and not want then
		  cev = bor(cev, input_bits)
	       end
	    elseif btest(rev, POLLIN) then
	       local ok, want = rpcall(fdt.input_evt, fdt.ctx, fdt._fd, false)
	       if ok and not want then
		  cev = bor(cev, input_bits)
	       end
	    end
	    -- Now if we're still registered, process write space
	    if fds[fd] and btest(rev, POLLOUT) then
	       local ok, want = rpcall(fdt.output_evt, fdt.ctx, fdt._fd)
	       if ok and not want then
		  cev = bor(cev, POLLOUT)
	       end
	    end
	 end
	 fds_to_process = fds_to_process - 1
	 if fds[fd] then
	    -- rearm appropriate events
	    pollfds_setslot(pfds, slot, fd, bclear(fds[fd].events, cev), 0);
	 end
      end
   end
   return nfds
end

local events = nil
local event_handles = {}
local _timer = "timer"
local _alarm = "alarm"

local function _msecs_to_timeval(msecs)
   local z = zero_timeval()
   z.useconds = msecs * 1000
   return z
end

local function _time_t_to_timeval(time_t)
   local z = zero_timeval()
   z.seconds = time_t
   return z
end

local function _insert_timer_event(tmr)
   if events == nil then
      events = tmr
      return
   end
   local evt, pevt = events
   while (evt and (evt.when < tmr.when)) do
      pevt = evt
      evt = evt.next
   end

   if evt == events then
      -- tmr is now earliest
      tmr.next = events
      tmr.next.prev = tmr
      events = tmr
      return
   end

   if evt == nil then
      -- tmr is now latest
      tmr.prev = pevt
      pevt.next = tmr
      return
   end
      
   -- tmr is to be inserted just before evt
   tmr.prev = pevt
   tmr.next = evt

   tmr.next.prev = tmr
   tmr.prev.next = tmr
end

local function _remove_timer_event(tmr)
   if tmr.prev == nil then
      events = tmr.next
      if tmr.next then
	 tmr.next.prev = nil
      end
   else
      tmr.prev.next = tmr.next
      if tmr.next then
	 tmr.next.prev = tmr.prev
      end
   end
   tmr.next, tmr.prev = nil, nil
end

local function _timer(timeout, handler, ctx)
   local handle = {}
   timeout = _msecs_to_timeval(timeout)
   local tmr = {
      type = _timer,
      timeout = timeout,
      handler = handler,
      ctx = ctx,
      when = gettimeofday() + timeout,
      handle = handle
   }
   _insert_timer_event(tmr)
   event_handles[handle] = tmr
   return handle
end

local function _alarm(wallclock, handler, ctx)
   local handle = {}
   local tmr = {
      type = _alarm,
      handler = handler,
      ctx = ctx,
      when = _time_t_to_timeval(wallclock),
      handle = handle
   }
   _insert_timer_event(tmr)
   event_handles[handle] = tmr
   return handle
end

local function _cancel_timer(handle)
   local tmr = event_handles[handle]
   assert(tmr, "Unknown handle passed to luxio.event.cancel_timer()")
   _remove_timer_event(tmr)
   event_handles[handle] = nil
end

local function _run()
   repeat
      local timeout = -1
      if events then
	 local now = gettimeofday()
	 timeout = ((events.when - now).useconds)/1000
      end
      _step(timeout)
      local fnow = gettimeofday()
      while events and events.when < fnow do
	 local evt = events
	 _remove_timer_event(evt)
	 local ok, ret = pcall(evt.handler, evt.ctx)
	 if ok and ret then
	    if evt.type == _timer then
	       evt.when = fnow + evt.timeout
	    elseif evt.type == _alarm then
	       evt.when = _time_t_to_timeval(ret)
	    else
	       error("Unknown time event type: " .. tostring(evt.type))
	    end
	    _insert_timer_event(evt)
	 end
      end
   until quitvals
   return unpack(quitvals)
end

local function _quit(...)
   quitvals = {...}
   error(quit_error)
end

return {
   register_fd = _register_fd,
   unregister_fd = _unregister_fd,
   set_events = _set_events,
   step = _step,
   run = _run,
   quit = _quit,

   -- Magic values passed to error_event
   nval = _nval,
   hup = _hup,
   err = _err,

   -- Timer related maguffins
   timer = _timer,
   alarm = _alarm,
   cancel_timer = _cancel_timer
}
