--[[--
Provides custom event broadcaster / listener mechanism to regular Lua objects.

All listeners receive the following fields in the parameter event table:

<code>event.name</code> (name of the event)

<code>event.target</code> (the listener itself)

<code>event.source</code> (the dispatcher)

Latest code: https://github.com/daveyang/EventDispatcher

@module EventDispatcher
@usage
local EvtD = require "EventDispatcher"

local dispatcher = EvtD()

-- listener as table
local listener = {
   eventName = function(event, ...)
      print(event.name, event.target, event.source)
   end
}

-- listener as function
local function listener(event, ...)
    print(event.name, event.target, event.source)
end

dispatcher:addEventListener( "eventName", listener ) -- or
dispatcher:on( "eventName", listener )

dispatcher:once( "eventName", listener )

dispatcher:hasEventListener( "eventName", listener )

dispatcher:dispatchEvent( { name="eventName" } ) -- or
dispatcher:dispatchEvent( "eventName" ) -- or
dispatcher:emit( { name="eventName" } ) -- or
dispatcher:emit( "eventName" )

dispatcher:removeEventListener( "eventName", listener )

dispatcher:removeAllListeners( "eventName" ) -- or
dispatcher:removeAllListeners()

dispatcher:printListeners()
]]


local EventDispatcher = {}

-- Initializes an object (this is automatically invoked and is considered a private method)
-- @param o table object to become event dispatcher
-- @return a table
function EventDispatcher:init(o)
    o = o or {}
    o._listeners = {}
    self.__index = self
    return setmetatable(o, self)
end

---------------------------------------------------------------------------

--- Checks if the event dispatcher has registered listener for the event eventName.
-- @param eventName event name (string)
-- @param listener object (table or function)
-- @return found status (boolean); if found also returns the index of listener object.
function EventDispatcher:hasEventListener(eventName, listener)
    if eventName == nil or #eventName == 0 or listener == nil then
        return false
    end

    local a = self._listeners
    if a == nil then
        return false
    end

    for i, o in next, a do
        if o ~= nil and o.evt == eventName and o.obj == listener then
            return true, i
        end
    end
    return false
end

---------------------------------------------------------------------------

--- Adds a listener for the event eventName. Optional runs once flag.
-- @param eventName event name (string)
-- @param listener object (table or function)
-- @param isOnce flag to specify the listener only runs once (boolean)
-- @return success/fail status (boolean); position of listener is also returned if false; position=0 if failed.
-- @see on
-- @see once
function EventDispatcher:addEventListener(eventName, listener, isOnce)
    if not isOnce then
        local found, pos = self:hasEventListener(eventName, listener)
        if found then
            return false, pos
        end
    end

    local a = self._listeners
    if a == nil then
        return false, 0
    end

    a[#a + 1] = {
        evt = eventName,
        obj = listener,
        isOnce = isOnce,
    }
    return true
end

--- 'on' is an alias of 'addEventListener'
EventDispatcher.on = EventDispatcher.addEventListener

---------------------------------------------------------------------------

--- Adds a one-time listener for the event eventName. Once the event is dispatched, the listener is removed.
-- @param eventName event name (string)
-- @param listener object (table or function)
-- @return success/fail status (boolean); position of listener is also returned if false; position=0 if failed.
-- @see addEventListener
-- @see on
function EventDispatcher:once(eventName, listener)
    return self:addEventListener(eventName, listener, true)
end

---------------------------------------------------------------------------

--- Dispatches an event, with optional extra parameters.
-- @param event the event (table, must have a 'name' key; e.g. { name="eventName" }, or as string)
-- @param ... optional extra parameters
-- @return dispatch status (boolean).
-- @see emit
function EventDispatcher:dispatchEvent(event, ...)
    if event == nil then
        return false
    end

    if type(event) == "table" then
        if event.name == nil or type(event.name) ~= "string" or #event.name == 0 then
            return false
        end
    elseif type(event) == "string" then
        if #event == 0 then
            return false
        end
        event = {
            name = event,
        }
    end

    local a = self._listeners
    if a == nil then
        return false
    end

    local dispatched = false
    for _, o in next, a do
        if o ~= nil and o.obj ~= nil and o.evt == event.name then
            event.target = o.obj
            event.source = self

            if type(o.obj) == "function" then
                o.obj(event, ...)
                if o.isOnce then
                    self:removeEventListener(event.name, o.obj, true)
                end
                dispatched = true
            elseif type(o.obj) == "table" then
                local f = o.obj[event.name]
                if f ~= nil then
                    f(event, ...)
                    if o.isOnce then
                        self:removeEventListener(event.name, o.obj, true)
                    end
                    dispatched = true
                end
            end
        end
    end
    return dispatched
end

--- 'emit' is an alias of 'dispatchEvent'
EventDispatcher.emit = EventDispatcher.dispatchEvent

---------------------------------------------------------------------------

--- Removes listener with the eventName event from the event dispatcher.
-- @param eventName event name (string)
-- @param listener object (table or function)
-- @return removal status (boolean).
-- @see removeAllListeners
function EventDispatcher:removeEventListener(eventName, listener)
    local found, pos = self:hasEventListener(eventName, listener)
    if found then
        table.remove(self._listeners, pos)
    end
    return found
end

---------------------------------------------------------------------------

--- Removes all listeners with the eventName event from the event dispatcher.
--- If the optional eventName is nil, all listeners are removed from the event dispatcher.
-- @param eventName event name (string)
-- @return removal status (boolean), with the number of listeners removed.
-- @see removeEventListener
function EventDispatcher:removeAllListeners(eventName)
    local a = self._listeners
    if a == nil then
        return false
    end

    if eventName == nil then
        local n = #self._listeners
        self._listeners = {}
        return true, n
    else
        local found = false
        local i = #a
        local n = 0

        while i > 0 do
            local o = a[i]
            if o ~= nil and o.evt == eventName then
                table.remove(a, i)
                found = true
                n = n + 1
            end
            i = i - 1
        end
        return found, n
    end
end

---------------------------------------------------------------------------

--- Prints the content of the _listeners array (for debugging).
-- Format: index, eventName, listener, isOnce.
function EventDispatcher:printListeners()
    local a = self._listeners
    if a == nil then
        return false
    end

    for i, o in next, a do
        if o ~= nil then
            print(i, o.evt, o.obj, o.isOnce)
        end
    end
end

---------------------------------------------------------------------------

-- Create syntactic sugar to automatically call init().
setmetatable(EventDispatcher, {
    __call = function(_, ...)
        return EventDispatcher:init(...)
    end,
})

return EventDispatcher
