--[[
    Hooking API (holding off on generalization until we see everything that needs to use it...
--]]

-- Install a handler that will be called before the original function and whose return value will decide if the original even needs to be called.
-- If the hook returns true it means that the hook handled the call entirely, and the original doesn't need calling.
-- ZO_PreHook can be called with or without an objectTable; if the argument is a string (the function name), it just uses _G
function ZO_PreHook(objectTable, existingFunctionName, hookFunction)
    if type(objectTable) == "string" then
        hookFunction = existingFunctionName
        existingFunctionName = objectTable
        objectTable = _G
    end
     
    local existingFn = objectTable[existingFunctionName]
    if existingFn ~= nil and type(existingFn) == "function" then    
        local newFn =   function(...)
                            if not hookFunction(...) then
                                return existingFn(...)
                            end
                        end

        objectTable[existingFunctionName] = newFn
    end
    return existingFn
end

function ZO_PostHook(objectTable, existingFunctionName, hookFunction)
    if type(objectTable) == "string" then
        hookFunction = existingFunctionName
        existingFunctionName = objectTable
        objectTable = _G
    end
     
    local existingFn = objectTable[existingFunctionName]
    if existingFn ~= nil and type(existingFn) == "function" then    
        local newFn =   function(...)
                            local returns = {existingFn(...)}
                            hookFunction(...)
                            return unpack(returns)
                        end

        objectTable[existingFunctionName] = newFn
    end
    return existingFn
end

function ZO_PreHookHandler(control, handlerName, hookFunction)
    local existingHandlerFunction = control:GetHandler(handlerName)
    local newHandlerFunction
    if existingHandlerFunction then
        newHandlerFunction = function(...)
            if not hookFunction(...) then
                return existingHandlerFunction(...)
            end
        end
    else
        newHandlerFunction = hookFunction
    end
    control:SetHandler(handlerName, newHandlerFunction)
    return existingHandlerFunction
end

function ZO_PostHookHandler(control, handlerName, hookFunction)
    local existingHandlerFunction = control:GetHandler(handlerName)
    local newHandlerFunction
    if existingHandlerFunction then
        newHandlerFunction = function(...)
            local returns = {existingHandlerFunction(...)}
            hookFunction(...)
            return unpack(returns)
        end
    else
        newHandlerFunction = hookFunction
    end
    control:SetHandler(handlerName, newHandlerFunction)
    return existingHandlerFunction
end

--where ... are the handler args after self
-- ZO_PropagateHandler(self:GetParent(), "OnMouseUp", button, upInside)
function ZO_PropagateHandler(propagateTo, handlerName, ...)
    if propagateTo then
        -- TODO: Determine whether handlers of any namespace should be called;
        --       new Control methods - GetNumHandlers and GetHandlerByIndex - would be required to do so.
        local handler = propagateTo:GetHandler(handlerName)
        if handler then
            handler(propagateTo, ...)
            return true
        end
    end
    return false
end

-- Propagates a call to the specified handler name from a control to each ancestor control
-- that defines a corresponding handler function until and excluding the owning window.
-- ZO_PropagateHandlerToAllAncestors("OnMouseUp", ...)
function ZO_PropagateHandlerToAllAncestors(handlerName, propagateFromControl, ...)
    local owningWindow = propagateFromControl:GetOwningWindow()
    local ancestorControl = propagateFromControl:GetParent()
    -- No semaphore mechanism is currently required to suppress reentrant calls when
    -- ancestor controls also inherit ZO_PropagateMouseOverBehaviorToAllAncestors
    -- because, at present, ZO_PropagateHandler only propagates events to handlers
    -- that have no namespace. A gating mechanism would be required should this
    -- paradigm ever change.
    while ancestorControl and ancestorControl ~= owningWindow do
        ZO_PropagateHandler(ancestorControl, handlerName, ...)
        ancestorControl = ancestorControl:GetParent()
    end
end

-- Propagates a call to the specified handler name from a control to the nearest ancestor control
-- that defines a corresponding handler function until and excluding the owning window.
-- ZO_PropagateHandlerToNearestAncestor("OnMouseUp", ...)
function ZO_PropagateHandlerToNearestAncestor(handlerName, propagateFromControl, ...)
    local owningWindow = propagateFromControl:GetOwningWindow()
    local ancestorControl = propagateFromControl:GetParent()
    while ancestorControl and ancestorControl ~= owningWindow and not ZO_PropagateHandler(ancestorControl, handlerName, ...) do
        ancestorControl = ancestorControl:GetParent()
    end
end

-- For when you want to propagate to the control's parent without breaking self out of the args
-- ZO_PropagateHandlerToParent("OnMouseUp", ...)
function ZO_PropagateHandlerToParent(handlerName, propagateFromControl, ...)
    ZO_PropagateHandler(propagateFromControl:GetParent(), handlerName, ...)
end

-- For when you want to propagate without breaking self out of the args
-- ZO_PropagateHandlerFromControl(self:GetParent():GetParent(), "OnMouseUp", ...)
function ZO_PropagateHandlerFromControl(propagateToControl, handlerName, propagateFromControl, ...)
    ZO_PropagateHandler(propagateToControl, handlerName, ...)
end

--[[
  Updates the metatable of the specified control to forward calls to any umimplemented
  methods to any of the specified control(s) that have a corresponding implementation.

  Invocation of a relayed method will return the return value of each target objects'
  implementation in the order that the target objects were specified.

  For example:

  -- Forward unimplemented method calls to the container control's child label control.
  local containerControl = self.control:GetNamedChild("PlayerContainer")
  local labelControl = containerControl:GetNamedChild("Name")
  ZO_ForwardUnimplementedMethodsForControl(containerControl, labelControl)

  -- Call label control-specific methods of the child label control via its parent container control.
  containerControl:SetFont("ZoFontGameBold")
  containerControl:SetText(playerName)
]]
do
    local UNIMPLEMENTED_METHOD_SENTINEL = {}

    local function GetOrCreateMethodRelayOrUnimplementedSentinel(key, targetObjects)
        -- Identify any target objects with a corresponding method implementation for this key.
        local objectsWithImplementations = {}
        for _, targetObject in ipairs(targetObjects) do
            local targetMethod = targetObject[key]
            if targetMethod and type(targetMethod) == "function" then
                table.insert(objectsWithImplementations, targetObject)
            end
        end

        if not next(objectsWithImplementations) then
            -- No target objects have a corresponding method implementation for this key.
            return UNIMPLEMENTED_METHOD_SENTINEL
        end

        -- Create a relay method that will forward the invocation of this method to
        -- the supporting target objects and return the first return value of each
        -- forwarded call in the order in which the methods are invoked.
        return function(...)
            local returnValues = {}
            for index, targetObject in ipairs(objectsWithImplementations) do
                -- Relay the call to the target object with a reference to the
                -- target object substituted in place of the 'self' parameter.
                local targetMethod = targetObject[key]
                returnValues[index] = targetMethod(targetObject, select(2, ...))
            end
            return unpack(returnValues)
        end
    end

    function ZO_ForwardUnimplementedMethodsForControl(sourceControl, ...)
        -- Wrap the source control's existing metatable indexer method so that keys not found
        -- in the source control are subsequently searched for within the target object(s).
        -- A relay method is created, cached, and returned for any target objects that have a
        -- method implementation for a given key; if no method implementations are found,
        -- a sentinel value is cached to prevent unnecessary lookups for that key.
        local targetObjects = {...}
        local relayMethods = {}
        local metaTable = getmetatable(sourceControl)
        local originalIndex = rawget(metaTable, "__index")
        local indexWrapper = function(tbl, key)
            if originalIndex then
                local value = originalIndex[key]
                if value ~= nil then
                    -- Return the corresponding value for existing keys.
                    return value
                end
            end

            local relayMethod = relayMethods[key]
            if relayMethod == nil then
                -- Create either a relay method that forwards this invocation to
                -- supporting target objects or a sentinel value that signals that
                -- no target objects have a corresponding implementation.
                relayMethod = GetOrCreateMethodRelayOrUnimplementedSentinel(key, targetObjects)
                relayMethods[key] = relayMethod
            end

            if relayMethod == UNIMPLEMENTED_METHOD_SENTINEL then
                -- An attempt was already made to index this key but no
                -- implementation was found. Suppress further attempts.
                return nil
            end

            return relayMethod
        end
        rawset(metaTable, "__index", indexWrapper)
    end
end