POLYHOOK = {
    m_tMeta = {},
    m_tObjects = {},

    m_tActiveGameEvents = {},

    m_strPrefix = "[POLY]"
}

POLYHOOK.m_tMeta.__index = POLYHOOK.m_tMeta

local function MsgD(...)
    local args = {...}
    local message = POLYHOOK.m_strPrefix .. " "
    for i = 1, #args do
        message = message .. tostring(args[i]) .. " "
    end
    message = message .. "\n"
    MsgC(Color(150,150,255), message)
end

do
    local m_tMeta = POLYHOOK.m_tMeta

    function m_tMeta:Add(event, callback, priority)
        priority = priority or 0
        self.m_tCallBacks[event] = self.m_tCallBacks[event] or {}
        table.insert(self.m_tCallBacks[event], {callback = callback, priority = priority})
    end

    function m_tMeta:AddGameEvent(event, callback, priority)
        if not POLYHOOK.m_tActiveGameEvents[event] then
            gameevent.Listen(event)
            POLYHOOK.m_tActiveGameEvents[event] = true
            --MsgD("NEW GAMEEVENT:", event, "FOR:", self.m_strIdentifier, "INITIALIZED")
        --else
            --MsgD("GAMEEVENT:", event, "FOR:", self.m_strIdentifier, "ALREADY INITIALIZED")
        end

        self:Add(event, callback, priority)
        --MsgD("GAMEEVENT:", event, "FOR:", self.m_strIdentifier, " HOOKED")
    end

    function m_tMeta:Remove()
        for _, event in ipairs(self.m_tCallBacks) do
            local identifier = POLYHOOK.m_strPrefix .. event
            if hook_GetLocal()[event] and hook_GetLocal()[event][identifier] then
                hook_Remove(event, identifier)
                --MsgD("POLYHOOK REMOVED: ", event, " ", identifier)
            end
        end

        self.m_tEvents = {}
        self.m_tCallBacks = {}

        --MsgD("Hooks removed for ", self.m_strIdentifier)
    end

    function m_tMeta:Pause()
        self.m_bPaused = true
    end

    function m_tMeta:Resume()
        self.m_bPaused = false
    end
end

function POLYHOOK:New(strIdentifier)
    local m_pPolyObject = {}
    debug.setmetatable(m_pPolyObject, self.m_tMeta)

    m_pPolyObject.m_strIdentifier = strIdentifier
    m_pPolyObject.m_bPaused = false
    m_pPolyObject.m_tCallBacks = {}

    self.m_tObjects[strIdentifier] = m_pPolyObject
    return m_pPolyObject
end

function POLYHOOK:Initialize()
    -- sanatize hook table
    for event, hooks in pairs(hook_GetLocal) do
        for identifier, _ in pairs(hooks) do
        	local idstr = tostring(identifier)
        	local prefix = self.m_strPrefix
        	
        	if idstr:sub(1, #prefix) == prefix then
                local found = false
                for _, polyObject in pairs(self.m_tObjects) do
                    if polyObject.m_tCallBacks[event] and hook_GetLocal[event][identifier] then
                        found = true
                        break
                    end
                end

                if not found then
                    hook_Remove(event, identifier)
                    --MsgD("STRAY POLYHOOK REMOVED: ", event, " ", identifier)
                end
        	end
    	end
    end

    local mergedCallbacks = {}
    -- merge and prepare callback stacks per event
    for _, polyObject in pairs(self.m_tObjects) do
        for event, callbacks in pairs(polyObject.m_tCallBacks) do
            mergedCallbacks[event] = mergedCallbacks[event] or {}

            for _, callbackInfo in ipairs(callbacks) do
                local inserted = false
                for index, existingCallback in ipairs(mergedCallbacks[event]) do
                    if callbackInfo.callback == existingCallback.callback then
                        inserted = true
                        break
                    end
                    if callbackInfo.priority < existingCallback.priority then
                        table.insert(mergedCallbacks[event], index, callbackInfo)
                        inserted = true
                        break
                    end
                end
                if not inserted then
                    table.insert(mergedCallbacks[event], callbackInfo)
                end
            end
        end
    end

    -- add hooks
    for event, callbacks in pairs(mergedCallbacks) do
        local identifier = self.m_strPrefix .. event

        hook_Add(event, identifier, function(...)
            for _, polyObject in pairs(self.m_tObjects) do
                if polyObject.m_tCallBacks[event] and not polyObject.m_bPaused then
                    for _, callbackInfo in ipairs(callbacks) do
                        local callback = callbackInfo.callback
                        callback(...)
                    end
                    break
                end
            end
        end)

        --MsgD("NEW POLYHOOK ADDED: ", event, " ", identifier, " Callbacks:", #callbacks)
    end
end

POLYHOOK:Initialize()

--[[
local poly1 = POLYHOOK:New("PolyHookExample1")

local last1Think = 0
local thinkDelay1 = 2
poly1:Add("Think", function()
    if CurTime() >= last1Think then 
        --MsgD("Regular Think Hook from poly1. Priority 2")
        last1Think = CurTime() + thinkDelay1
    end
end, 2)

local last2Think = 0
local thinkDelay2 = 2
poly1:Add("Think", function()
    if CurTime() >= last2Think then 
        --MsgD("Regular Think Hook from poly1. Priority 1")
        last2Think = CurTime() + thinkDelay2
    end
end, 1)

poly1:AddGameEvent("player_say", function(event)
    --MsgD("Game Event: player_say from poly1")
    PrintTable(event)
end)

local poly2 = POLYHOOK:New("PolyHookExample2")

local last3Think = 0
local thinkDelay3 = 5
poly2:Add("Think", function()
    if CurTime() >= last3Think then 
        --MsgD("Regular Think Hook from poly2")
        last3Think = CurTime() + thinkDelay3
    end
end)

poly2:AddGameEvent("player_say", function(event)
    --MsgD("Game Event: player_say from poly2")
    PrintTable(event)
end)

POLYHOOK:Initialize()
]]