-- Sentinel category values applicable to any ZO_PrioritizedVisibility instance.
ZO_PRIORITIZED_VISIBILITY_CATEGORIES =
{
    NONE = 0,
    ALL = 2 ^ (ZO_INTEGER_53_MAX_BITS - 1), -- Highest order bit that can be combined with lower bits without an overflow condition.
}

local PrioritizedVisibilityObjectInfo = ZO_InitializingObject:Subclass()

function PrioritizedVisibilityObjectInfo:Initialize(object, priority, categoryFlags, descriptor)
    self.object = object
    self.priority = priority
    self.categoryFlags = categoryFlags
    self.descriptor = descriptor

    self:SetHidden(true)
    self:SetRequestedHidden(true)
end

-- Returns true if this object info is neither hidden by request nor a member of any of the specified suppressed categories.
function PrioritizedVisibilityObjectInfo:CanShow(suppressedCategoriesMask)
    return not (self.requestedHidden or ZO_FlagHelpers.MasksShareAnyFlag(self.categoryFlags, suppressedCategoriesMask))
end

-- Returns true if this object info has a higher priority than the specified object info.
function PrioritizedVisibilityObjectInfo:ComparePriority(objectInfo)
    return self.priority - objectInfo.priority
end

function PrioritizedVisibilityObjectInfo:GetDescriptor()
    return self.descriptor
end

function PrioritizedVisibilityObjectInfo:GetCategoryFlags()
    return self.categoryFlags
end

function PrioritizedVisibilityObjectInfo:GetPriority()
    return self.priority
end

function PrioritizedVisibilityObjectInfo:GetObject()
    return self.object
end

-- Returns true if this object info has requested to be hidden.
function PrioritizedVisibilityObjectInfo:IsRequestedHidden()
    return self.requestedHidden
end

-- Shows or hides the underlying object or control.
function PrioritizedVisibilityObjectInfo:SetHidden(hidden)
    self.object:SetHidden(hidden)
end

-- Sets or clears the request to hide flag.
function PrioritizedVisibilityObjectInfo:SetRequestedHidden(hidden)
    self.requestedHidden = hidden
end

ZO_PrioritizedVisibility = ZO_InitializingCallbackObject:Subclass()

function ZO_PrioritizedVisibility:Initialize()
    self.prioritizedObjects = {}
    self.registeredObjects = {}
    self.suppressedCategories = {}
    self.suppressedCategoriesMask = 0

    local function PriorityComparator(left, right)
        local leftObject = self.registeredObjects[left]
        local rightObject = self.registeredObjects[right]
        return rightObject:ComparePriority(leftObject) > 0
    end
    self.priorityComparator = PriorityComparator
end

-- 'object' can be a control or an object that implements a 'SetHidden' method.
-- Note that objects are immediately hidden when added.
function ZO_PrioritizedVisibility:Add(object, priority, categoryFlags, descriptor)
    -- Add all objects to the sentinel "All" category.
    categoryFlags = ZO_FlagHelpers.SetMaskFlag(categoryFlags or ZO_PRIORITIZED_VISIBILITY_CATEGORIES.NONE, ZO_PRIORITIZED_VISIBILITY_CATEGORIES.ALL)

    self.arePrioritizedObjectsDirty = true
    self.registeredObjects[object] = PrioritizedVisibilityObjectInfo:New(object, priority, categoryFlags, descriptor)

    -- Create any suppressed category descriptor tables for this object's category
    -- flag(s) that do not already exist.
    for categoryFlag in ZO_FlagHelpers.MaskHasFlagsIterator(categoryFlags) do
        if not self.suppressedCategories[categoryFlag] then
            self.suppressedCategories[categoryFlag] = {}
        end
    end
end

-- Returns the object info associated with the specified object.
function ZO_PrioritizedVisibility:GetObjectInfo(object)
    local objectInfo = self.registeredObjects[object]
    return objectInfo
end

-- Returns true if any of the specified category or categories is/are suppressed.
function ZO_PrioritizedVisibility:IsCategorySuppressed(categoriesMask)
    return ZO_FlagHelpers.MasksShareAnyFlag(self.suppressedCategoriesMask, categoriesMask)
end

-- Returns true if the specified object is the highest priority object that has
-- requested to be shown and is not a member of any suppressed categories.
function ZO_PrioritizedVisibility:IsHidden(object)
    return not self:IsHighestPriorityVisibleObject(object)
end

-- Returns true if the specified object is the highest priority object that has
-- requested to show and which is not a member of any suppressed categories.
function ZO_PrioritizedVisibility:IsHighestPriorityVisibleObject(object)
    return self.highestPriorityVisibleObject and object == self.highestPriorityVisibleObject:GetObject()
end

-- Sets or clears the specified category as suppressed.
-- Note that 'descriptor' should be a globally unique identifier of the system issuing the request.
function ZO_PrioritizedVisibility:SetCategorySuppressed(suppressed, categoryFlag, descriptor)
    if not internalassert(descriptor, "Parameter 'descriptor' is required.") then
        return false
    end

    local previousSuppressedCategoriesMask = self.suppressedCategoriesMask

    -- Add or remove descriptor for this suppressed category.
    local suppressedCategoryDescriptors = self.suppressedCategories[categoryFlag]
    if suppressed then
        suppressedCategoryDescriptors[descriptor] = true
    else
        suppressedCategoryDescriptors[descriptor] = nil
    end

    -- Update the suppressed category mask.
    if next(suppressedCategoryDescriptors) then
        self.suppressedCategoriesMask = ZO_FlagHelpers.SetMaskFlag(self.suppressedCategoriesMask, categoryFlag)
    else
        self.suppressedCategoriesMask = ZO_FlagHelpers.ClearMaskFlag(self.suppressedCategoriesMask, categoryFlag)
    end

    if previousSuppressedCategoriesMask ~= self.suppressedCategoriesMask then
        -- The suppressed categories changed; evaluate the new, highest priority visible object.
        self:UpdateHighestPriorityVisibleObject()
    end

    return true
end

-- Suppresses or unsuppresses the specified categories.
-- Note that 'descriptor' should be a globally unique identifier of the system issuing the request.
function ZO_PrioritizedVisibility:SetCategoriesSuppressed(suppressed, categoryMask, descriptor)
    -- Iterate over all registered categories.
    for categoryFlag in ZO_FlagHelpers.MaskHasFlagsIterator(categoryMask) do
        self:SetCategorySuppressed(suppressed, categoryFlag, descriptor)
    end

    return true
end

-- If 'suppressed' is true, suppresses the specified categories and unsuppresses all other categories;
-- otherwise this unsuppresses the specified categories and suppresses all other categories.
-- Note that 'descriptor' should be a globally unique identifier of the system issuing the request.
function ZO_PrioritizedVisibility:SetSuppressedCategoriesMask(suppressed, categoryMask, descriptor)
    -- Iterate over all registered categories.
    for categoryFlag in pairs(self.suppressedCategories) do
        local suppressCategory
        if suppressed then
            -- Suppress the category if specified in the mask; otherwise unsuppress the category.
            suppressCategory = BitAnd(categoryFlag, categoryMask) ~= 0
        else
            -- Unsuppress the category if specified in the mask; otherwise suppress the category.
            suppressCategory = BitAnd(categoryFlag, categoryMask) == 0
        end
        self:SetCategorySuppressed(suppressCategory, categoryFlag, descriptor)
    end

    return true
end

-- Requests to show or hide the specified object or control.
-- Note that a request to show is granted once the object is
-- the highest priority object that requested to show and is
-- not a member of any suppressed category.
function ZO_PrioritizedVisibility:SetHidden(object, hidden)
    local objectInfo = self.registeredObjects[object]
    if objectInfo:IsRequestedHidden() == hidden then
        return
    end

    -- Update the requested hidden flag for this object.
    objectInfo:SetRequestedHidden(hidden)

    -- Choose the new, highest priority, visible and unsuppressed object.
    self:UpdateHighestPriorityVisibleObject()
end

-- Rebuilds the prioritized objects array in order of highest to lowest priority.
function ZO_PrioritizedVisibility:UpdatePrioritizedObjects()
    ZO_ClearNumericallyIndexedTable(self.prioritizedObjects)

    for _, objectInfo in pairs(self.registeredObjects) do
        table.insert(self.prioritizedObjects, objectInfo:GetObject())
    end

    table.sort(self.prioritizedObjects, self.priorityComparator)
end

function ZO_PrioritizedVisibility:UpdateHighestPriorityVisibleObject()
    if self.arePrioritizedObjectsDirty then
        -- New objects have been registered; update the prioritized object array.
        self:UpdatePrioritizedObjects()
        self.arePrioritizedObjectsDirty = nil
    end

    -- Evaluate objects in order of highest to lowest priority
    -- until an object that is valid for showing is found.
    local highestPriorityVisibleObject = nil
    local suppressedCategoriesMask = self.suppressedCategoriesMask
    for _, object in ipairs(self.prioritizedObjects) do
        local objectInfo = self.registeredObjects[object]
        if objectInfo:CanShow(suppressedCategoriesMask) then
            -- This object has requested to show and is not a
            -- member of any suppressed categories.
            highestPriorityVisibleObject = objectInfo
            break
        end
    end

    local highestPriorityVisibleObjectChanged = self.highestPriorityVisibleObject ~= highestPriorityVisibleObject
    if highestPriorityVisibleObjectChanged then
        local previousObjectInfo = self.highestPriorityVisibleObject
        local newObjectInfo = highestPriorityVisibleObject
        self:FireCallbacks("VisibleObjectChanged", newObjectInfo, previousObjectInfo)

        if self.highestPriorityVisibleObject then
            -- Hide the previously highest priority visible object.
            self.highestPriorityVisibleObject:SetHidden(true)
        end

        -- Assign the new highest priority visible object.
        self.highestPriorityVisibleObject = highestPriorityVisibleObject

        if self.highestPriorityVisibleObject then
            -- Show the new highest priority visible object.
            self.highestPriorityVisibleObject:SetHidden(false)
        end
    end
end