local MAJOR, MINOR = "Olivine:Mediator-1.0", 1
-- Get a reference to the package information if any
local APkg = Apollo.GetPackage(MAJOR)
-- If there was an older version loaded we need to see if this is newer
if APkg and (APkg.nVersion or 0) >= MINOR then
    return
end

-------------------------------------------------------------------------------
--- Olivine-Labs Mediator
-------------------------------------------------------------------------------

local mediatorProto

local function Subscriber(fn, options)
    return {
        options = options or {},
        fn = fn,
        channel = nil,
        id = math.random(1000000000), -- sounds reasonable, rite?
        update = function(self, options)
            if options then
                self.fn = options.fn or self.fn
                self.options = options.options or self.options
            end
        end
    }
end

-- Channel class and functions --

local function Channel(namespace, parent)
    return {
        stopped = false,
        namespace = namespace,
        callbacks = {},
        channels = {},
        parent = parent,

        addSubscriber = function(self, fn, options)
            local callback = Subscriber(fn, options)
            local priority = (#self.callbacks + 1)

            options = options or {}

            if options.priority and
                options.priority >= 0 and
                options.priority < priority
            then
                    priority = options.priority
            end

            table.insert(self.callbacks, priority, callback)

            return callback
        end,

        getSubscriber = function(self, id)
            for i=1, #self.callbacks do
                local callback = self.callbacks[i]
                if callback.id == id then return { index = i, value = callback } end
            end
            local sub
            for _, channel in pairs(self.channels) do
                sub = channel:getSubscriber(id)
                if sub then break end
            end
            return sub
        end,

        setPriority = function(self, id, priority)
            local callback = self:getSubscriber(id)

            if callback.value then
                table.remove(self.callbacks, callback.index)
                table.insert(self.callbacks, priority, callback.value)
            end
        end,

        addChannel = function(self, namespace)
            self.channels[namespace] = Channel(namespace, self)
            return self.channels[namespace]
        end,

        hasChannel = function(self, namespace)
            return namespace and self.channels[namespace] and true
        end,

        getChannel = function(self, namespace)
            return self.channels[namespace] or self:addChannel(namespace)
        end,

        removeSubscriber = function(self, id)
            local callback = self:getSubscriber(id)

            if callback and callback.value then
                for _, channel in pairs(self.channels) do
                    channel:removeSubscriber(id)
                end

                return table.remove(self.callbacks, callback.index)
            end
        end,

        publish = function(self, result, ...)
            for i = 1, #self.callbacks do
                local callback = self.callbacks[i]

                -- if it doesn't have a predicate, or it does and it's true then run it
                if not callback.options.predicate or callback.options.predicate(...) then
                     -- just take the first result and insert it into the result table
                    local value, continue = callback.fn(...)

                    if value then table.insert(result, value) end
                    if not continue then return result end
                end
            end

            if parent then
                return parent:publish(result, ...)
            else
                return result
            end
        end
    }
end

-- Mediator class and functions --

mediatorProto = setmetatable(
{
    Channel = Channel,
    Subscriber = Subscriber
},
{
    __call = function (fn, options)
        return {
            channel = Channel('root'),

            getChannel = function(self, channelNamespace)
                local channel = self.channel

                for i=1, #channelNamespace do
                    channel = channel:getChannel(channelNamespace[i])
                end

                return channel
            end,

            subscribe = function(self, channelNamespace, fn, options)
                return self:getChannel(channelNamespace):addSubscriber(fn, options)
            end,

            getSubscriber = function(self, id, channelNamespace)
                return self:getChannel(channelNamespace):getSubscriber(id)
            end,

            removeSubscriber = function(self, id, channelNamespace)
                return self:getChannel(channelNamespace):removeSubscriber(id)
            end,

            publish = function(self, channelNamespace, ...)
                return self:getChannel(channelNamespace):publish({}, ...)
            end
        }
    end
})

Apollo.RegisterPackage(mediatorProto, MAJOR, MINOR, {})
