-- Basic event loop library for computer craft
--
-- Example usage:
--
-- local events = require('apis/eventloop')()
--
-- local disposeRedstoneEvent = events.register("redstone", function()
--   print("Redstone signal received!")
-- end)
--
-- events.register("key_up", function(k)
--   if k == keys.q then
--     disposeRedstoneEvent()
--     events.stopLoop()
--   end
-- end)
--
-- events.runLoop();
local EVENTLOOP_COUNTER_KEY = '__trapos_next_eventloop_id';

local function nextEventLoopId()
  local id = tonumber(_G[EVENTLOOP_COUNTER_KEY]) or 1;
  _G[EVENTLOOP_COUNTER_KEY] = id + 1;
  return id;
end

local function noop()
end

local STOP = '@libeventloop/STOP_HANDLER_SUBSCRIPTION';

local function createEventLoop(opts)
  opts = opts or {};
  local api = {}

  local runningLoop = false;
  local eventloop_id = nextEventLoopId();
  local stopRequested = false;

  local shouldCheckHandlers = true;

  local handlersCounter = 0;
  local allHandlers = {};

  local runningHandlers = nil;
  local unregisterQueue = {};

  -- used when setTimeout are registered when runningLoop is false
  local runningTimeoutHandler = false;
  local timeoutFactoryCounter = 0;
  local nextTimeoutFactoryId = 1;
  local timeoutFactories = {};
  local removeTimeoutQueue = {};

  -- used at runtime
  local timeoutHandlersCounter = 0;
  local timeoutHandlers = {};

  -- onStop handlers
  local onStopHandlerCounter = 0;
  local nextOnStopHandlerId = 1;
  local onStopHandlers = {};

  -- onStart handlers
  local nextOnStartHandlerId = 1;
  local onStartHandlers = {};

  local function reportError(eventName, err)
    if type(opts.onError) == 'function' then
      pcall(opts.onError, eventName, err);
    end
  end

  local function dispatchHandler(eventName, handler, ...)
    if type(opts.onError) ~= 'function' then
      return true, handler(...);
    end

    local ok, result = pcall(handler, ...);
    if not ok then
      reportError(eventName, result);
      return false, nil;
    end
    return true, result;
  end

  local function resetOnStartHandlers()
    nextOnStartHandlerId = 1;
    onStartHandlers = {};
  end

  local function resetOnStopHandlers()
    onStopHandlerCounter = 0;
    nextOnStopHandlerId = 1;
    onStopHandlers = {};
  end

  local function addOnStopHandler(h)
    local id = nextOnStopHandlerId;

    nextOnStopHandlerId = nextOnStopHandlerId + 1;
    onStopHandlerCounter = onStopHandlerCounter + 1;

    onStopHandlers[id] = h;

    return id;
  end

  local function execOnStartHandlers()
    for _, h in pairs(onStartHandlers) do
      h();
    end
    resetOnStartHandlers();
  end

  local function execOnStopHandlers()
    for _, h in pairs(onStopHandlers) do
      h();
    end
    resetOnStopHandlers();
  end

  local function shouldStop()
    if shouldCheckHandlers then
      return timeoutHandlersCounter + handlersCounter == 0 and timeoutFactoryCounter == 0;
    end

    return false
  end

  local function insertTimeoutHandler(id, handler)
    timeoutHandlers[id] = handler;
    timeoutHandlersCounter = timeoutHandlersCounter + 1
  end

  local function removeTimeoutHandler(id)
    local function removeFn()
      if timeoutHandlers[id] then
        timeoutHandlers[id] = nil;
        timeoutHandlersCounter = timeoutHandlersCounter - 1

        if shouldStop() then
          api.stopLoop();
        end
      end
    end

    table.insert(removeTimeoutQueue, removeFn);
  end

  local function resetFactories()
    timeoutFactoryCounter = 0;
    nextTimeoutFactoryId = 1;
    timeoutFactories = {}
  end

  local function applyTimeoutFactories()
    for _, f in pairs(timeoutFactories) do
      local timerId, h = f()
      insertTimeoutHandler(timerId, h)
    end

    resetFactories();
  end

  local function resetAll()
    runningLoop = false;
    stopRequested = false;
    handlersCounter = 0;
    allHandlers = {};
    runningHandlers = nil;
    unregisterQueue = {};

    resetFactories();

    for k, _ in pairs(timeoutHandlers) do
      os.cancelTimer(k);
    end

    runningTimeoutHandler = false;
    timeoutHandlersCounter = 0;
    timeoutHandlers = {};
    removeTimeoutQueue = {};

    resetOnStartHandlers();
    resetOnStopHandlers();
  end

  local function flushUnregisterQueue()
    if #unregisterQueue == 0 then
      return
    end

    for _, f in pairs(unregisterQueue) do
      f()
    end

    unregisterQueue = {}
  end

  local END_OF_LOOP = '@libeventloop/END_OF_LOOP/' .. eventloop_id

  api.STOP = STOP

  -- isRunningLoop
  function api.isRunningLoop()
    return runningLoop;
  end

  -- stopLoop
  function api.stopLoop()
    if api.isRunningLoop() then
      if runningHandlers or runningTimeoutHandler then
        stopRequested = true;
      else
        os.queueEvent(END_OF_LOOP)
      end
    else
      error("libeventloop error: loop is already stopped")
    end
  end

  -- unregister
  function api.unregister(eventName, handler)
    assert(type(eventName) == 'string', 'bad argument #1 (string expected)')
    assert(type(handler) == 'function', 'bad argument #2 (function expected)')

    local function removeHandler()
      local handlers = allHandlers[eventName]

      if not handlers then
        error("libeventloop error: no handler registered for the '" .. eventName .. "' event")
      end

      if handlers[handler] then
        handlers[handler] = nil
        handlersCounter = handlersCounter - 1;
        if shouldStop() then
          api.stopLoop();
        end
      end
    end

    if runningHandlers then
      table.insert(unregisterQueue, function()
        return removeHandler()
      end)
      return
    end

    return removeHandler()
  end

  -- register
  function api.register(eventName, handler)
    assert(type(eventName) == 'string', 'bad argument #1 (string expected)')
    assert(type(handler) == 'function', 'bad argument #2 (function expected)')

    if not allHandlers[eventName] then
      allHandlers[eventName] = {}
    end

    local handlers = allHandlers[eventName]
    if handlers[handler] then
      error("libeventloop error: handler already registered for event '" .. eventName .. "'")
    end

    handlers[handler] = handler;
    handlersCounter = handlersCounter + 1;

    return function()
      api.unregister(eventName, handler)
    end
  end

  -- runLoop
  function api.runLoop(noCheckHandlers)
    if api.isRunningLoop() then
      error("libeventloop error: event loop is already ran")
    end

    shouldCheckHandlers = not noCheckHandlers

    applyTimeoutFactories()

    if shouldStop() then
      if onStopHandlerCounter > 0 then
        execOnStopHandlers()
      end
      return
    end

    runningLoop = true;
    execOnStartHandlers()

    while true do
      local packed = table.pack(os.pullEventRaw())
      local eventName = table.remove(packed, 1)

      if eventName == 'timer' then
        -- setTimeout handlers
        local timerId = packed[1];

        runningTimeoutHandler = true
        for k, h in pairs(timeoutHandlers) do
          if k == timerId then
            removeTimeoutHandler(k)
            dispatchHandler('timer', h);
          end
        end
        runningTimeoutHandler = false

        for _, removeFn in ipairs(removeTimeoutQueue) do
          removeFn()
        end
        removeTimeoutQueue = {}

        applyTimeoutFactories()
      else
        -- regular event handlers
        local handlers = allHandlers[eventName]
        if handlers then
          runningHandlers = eventName
          for _, handler in pairs(handlers) do
            local ok, result_handler = dispatchHandler(eventName, handler, table.unpack(packed))
            if ok and result_handler == api.STOP then
              api.unregister(eventName, handler)
            end
          end
          runningHandlers = nil
          flushUnregisterQueue()
        end

      end

      if stopRequested or eventName == END_OF_LOOP or (shouldCheckHandlers and eventName == 'terminate') then
        execOnStopHandlers()
        resetAll()
        break
      end
    end
  end

  -- startLoop
  api.startLoop = api.runLoop

  -- setTimeout
  function api.setTimeout(handler, seconds)
    seconds = math.abs(seconds or 0);

    assert(type(handler) == 'function', 'bad argument #1 (function expected)')
    assert(type(seconds) == 'number', 'bad argument #2 (number expected)')

    if api.isRunningLoop() and not runningTimeoutHandler then
      local timerId = os.startTimer(seconds)

      insertTimeoutHandler(timerId, handler)

      return function()
        -- clearTimeout
        if timeoutHandlers[timerId] then
          removeTimeoutHandler(timerId)
          os.cancelTimer(timerId);
        end
      end
    else
      local timeoutFactoryId = nextTimeoutFactoryId;
      nextTimeoutFactoryId = nextTimeoutFactoryId + 1

      local timerId = nil;

      local factory = function()
        timerId = os.startTimer(seconds)
        return timerId, handler;
      end

      timeoutFactories[timeoutFactoryId] = factory
      timeoutFactoryCounter = timeoutFactoryCounter + 1;

      return function()
        -- clearTimeout
        if timerId == nil and timeoutFactories[timeoutFactoryId] then
          timeoutFactories[timeoutFactoryId] = nil
          timeoutFactoryCounter = timeoutFactoryCounter - 1;
        elseif timeoutHandlers[timerId] then
          removeTimeoutHandler(timerId)
          os.cancelTimer(timerId);
        end
      end
    end
  end

  -- onStop
  function api.onStop(handler)
    assert(type(handler) == 'function', 'bad argument #1 (function expected)')

    local handlerId = addOnStopHandler(handler);

    return function()
      local h = onStopHandlers[handlerId];
      if h then
        h();
        onStopHandlers[handlerId] = nil
        onStopHandlerCounter = onStopHandlerCounter - 1;
      end
    end
  end

  -- onStart
  function api.onStart(handler)
    assert(type(handler) == 'function', 'bad argument #1 (function expected)');

    if api.isRunningLoop() then
      handler();
      return noop;
    else
      local onStartId = nextOnStartHandlerId;
      nextOnStartHandlerId = nextOnStartHandlerId + 1;

      onStartHandlers[onStartId] = handler

      return function()
        if onStartHandlers[onStartId] then
          onStartHandlers[onStartId] = nil;
        end
      end
    end
  end

  return api;
end

return createEventLoop;
