ZO_FadingStationaryControlBuffer = ZO_Object:Subclass()

local START_FULLY_OPAQUE = 1
local START_AT_BEGINNING = 2
local START_FADE_IMMEDIATELY = 3

local CONTROL_TYPE_ENTRY = 1
local CONTROL_TYPE_ITEM = 2

local FLUSH_UPDATE_TIME_MS = 200
local FADING_ENTRY_OFFSET_TIME_MS = 67
local DEFAULT_MAX_DISPLAYED_ENTRIES = 4

--[[ Public API ]]--
function ZO_FadingStationaryControlBuffer:New(...)
    local fadingStationaryControlBuffer = ZO_Object.New(self)
    fadingStationaryControlBuffer:Initialize(...)
    return fadingStationaryControlBuffer
end

function ZO_FadingStationaryControlBuffer:Initialize(control, maxDisplayedEntries, fadeAnimationName, iconAnimationName, containerAnimationName, anchor, controllerType)
    self.control = control
    self.entryPools = {}
    self.headerPools = {}
    self.linePools = {}
    self.queue = {}
    self.templates = {}
    self.activeEntries = {}
    self.queuedTimedEntries = {}
    self.maxDisplayedEntries = maxDisplayedEntries or DEFAULT_MAX_DISPLAYED_ENTRIES
    self.maxLinesPerEntry = nil
    self.fadeAnimationName = fadeAnimationName
    self.iconAnimationName = iconAnimationName
    self.anchor = anchor or ZO_Anchor:New(TOP)
    self.currentNumDisplayedEntries = 0
    self.currentlyFadingEntries = 0
    self.queuedBatches = {}
    self.currentEntries = {}
    self.nextTimeToFlushMS = 0
    self.doesContainsEntries = false
    self.containerShowTimeMs = 5000
    self.containerStartTimeMs = 0
    self.emptyDeltaTime = 0
    local offsetY = select(6, self.control:GetAnchor())
    self.resetPositionY = offsetY
    self.controllerType = controllerType
    self.additionalEntrySpacingY = 0
    self.paused = false

    self:InitializeContainerAnimations(containerAnimationName)

    EVENT_MANAGER:RegisterForUpdate(string.format("%s%s", "ZO_FadingStationaryControlBuffer", controllerType), 30, function(...) self:OnUpdateBuffer(...) end)
end

function ZO_FadingStationaryControlBuffer:OnUpdateBuffer(timeMs)
    if self.paused then
        return
    end

    if timeMs > self.nextTimeToFlushMS then
        self:FlushEntries()
        self.nextTimeToFlushMS = timeMs + FLUSH_UPDATE_TIME_MS
    end

    if self.doesContainsEntries then
        local deltaContainerTime = timeMs - self.containerStartTimeMs
        if deltaContainerTime > self.containerShowTimeMs then
            self.doesContainsEntries = false
            self.fadeTimeline:PlayFromStart()
        end
    end

    local numQueuedTimedEntries = #self.queuedTimedEntries
    for i = numQueuedTimedEntries, 1, -1 do
        if self.queuedTimedEntries[i].fadeStartDelayMs < timeMs then
            self.queuedTimedEntries[i].control.fadeIconTimeline:PlayFromStart()
            table.remove(self.queuedTimedEntries, i)
        end
    end
end

function ZO_FadingStationaryControlBuffer:Pause()
    if not self.paused then
        self.paused = true
        self.pauseTimeMS = GetFrameTimeMilliseconds()
    end
end

function ZO_FadingStationaryControlBuffer:Resume()
    if self.paused then
        self.paused = false
        local timePausedMS = GetFrameTimeMilliseconds() - self.pauseTimeMS
        self.pauseTimeMS = nil

        if self.containerStartTimeMs ~= 0 then
            self.containerStartTimeMs = self.containerStartTimeMs + timePausedMS
        end

        for i, queuedTimedEntry in ipairs(self.queuedTimedEntries) do
            queuedTimedEntry.fadeStartDelayMs = queuedTimedEntry.fadeStartDelayMs + timePausedMS
        end
    end
end

function ZO_FadingStationaryControlBuffer:AddEntry(templateName, entry)
    if self.templates[templateName] then
        entry.templateName = templateName
        table.insert(self.queue, entry)
    end
end

function ZO_FadingStationaryControlBuffer:SetContainerShowTime(time)
    self.containerShowTimeMs = time
end

function ZO_FadingStationaryControlBuffer:AddTemplate(templateName, templateData)
    assert(templateData.equalityCheck)
    self.templates[templateName] = templateData
end

function ZO_FadingStationaryControlBuffer:SetAdditionalEntrySpacingY(additionalSpacingY)
    self.additionalEntrySpacingY = additionalSpacingY
end

--[[ Private API ]]--
    
function ZO_FadingStationaryControlBuffer:GetActiveEntryIndex(entryControl)
    for i, entry in ipairs(self.activeEntries) do
        if entry == entryControl then
            return i
        end
    end
end

do
    local function OnContainerFadeStop(timeline)
        --fadeout anim is complete, go back to the ready state
        local isValid, point, relTo, relPoint = timeline.control:GetAnchor()
        timeline.control:SetAnchor(point, relTo, relPoint, 0, timeline.controlBuffer.resetPositionY)
        timeline.controlBuffer:ReleaseAllControls()
        timeline.controlBuffer:RefreshBatchAfterRelease()
    end

    function ZO_FadingStationaryControlBuffer:InitializeContainerAnimations(containerAnimationName)

        local control = self.control
        self.fadeTimeline = ANIMATION_MANAGER:CreateTimelineFromVirtual(containerAnimationName, control)

        self.fadeTimeline.controlBuffer = self

        self.fadeTimeline:Stop()

        self.fadeTimeline.control = control
        self.fadeTimeline:SetHandler("OnStop", OnContainerFadeStop)

        control.fadeTimeline = self.fadeTimeline
    end
end

local function ReleaseAllChildren(control)
    local childCount = control:GetNumChildren()
    for i = 1, childCount do
        local child = control:GetChild(i)
        if child.pool then -- Could evaluate to false if the child is not managed by this class.
            child.pool:ReleaseObject(child.key)
        end
    end
end

function ZO_FadingStationaryControlBuffer:ReleaseAllControls()
    while #self.activeEntries > 0 do
        self:ReleaseControl(self.activeEntries[1])
    end
end

function ZO_FadingStationaryControlBuffer:RefreshBatchAfterRelease()
    if self.currentNumDisplayedEntries == 0 then
        self.lastAnchoredEntry = nil
        self:DisplayBatches()
    end
end

function ZO_FadingStationaryControlBuffer:RefreshBatchAfterFading()
    -- incase we are displaying a large batch of items and collect an extra one while the animation is playing
    -- we want to wait for the current animation to finish before moving onto the next set
    -- which we call here
    if  self.currentlyFadingEntries <= 0 then
        self:DisplayBatches()
    end
end

function ZO_FadingStationaryControlBuffer:ReleaseControl(alertControl)
    if alertControl.type == CONTROL_TYPE_ENTRY then
        self.currentNumDisplayedEntries = self.currentNumDisplayedEntries - 1
    end

    ReleaseAllChildren(alertControl)
    alertControl.pool:ReleaseObject(alertControl.key)

    if alertControl.type == CONTROL_TYPE_ENTRY then
        local entryIndex = self:GetActiveEntryIndex(alertControl)
        if entryIndex then
            table.remove(self.activeEntries, entryIndex)
        end
    end
end

function ZO_FadingStationaryControlBuffer:TryConcatWithExistingEntry(templateName, entry, tableLocation)
    local templateData = self.templates[templateName]

    local tableLocation = tableLocation or self.activeEntries
    local handled = false
    for _, activeEntryControl in ipairs(tableLocation) do
        local activeEntry = activeEntryControl.entry
        if templateName == activeEntry.templateName then
            -- Are both headers equal?
            if activeEntry.header and entry.header and templateData.headerEqualityCheck and templateData.headerEqualityCheck(activeEntry.header, entry.header) then
                if entry.lines then
                    -- Are all lines equal?
                    if activeEntry.lines and templateData.equalityCheck(activeEntry.lines, entry.lines) then
                        templateData.equalitySetup(self, activeEntry, entry)
                        activeEntryControl.setupTime = GetFrameTimeMilliseconds()
                        handled = true
                    end
                end
            -- Headers are either not equal or not present.  Are all lines equal?
            elseif activeEntry.lines and entry.lines and templateData.equalityCheck(activeEntry.lines, entry.lines) then
                templateData.equalitySetup(self, activeEntry, entry)
                handled = true
            end

            if handled then
                break
            end
        end
    end

    return handled
end

function ZO_FadingStationaryControlBuffer:CanDisplayMore()
    if self.fadeTimeline:IsPlaying() then
        return false
    end

    if self.doesContainsEntries and self.currentlyFadingEntries > 0 then
        return false
    end
    
    return self:CanDisplayEntry()
end

-- supplimental check for adding entries from a batch
-- we already know they are ok to go, but now we need to make sure we don't over add items
function ZO_FadingStationaryControlBuffer:CanDisplayEntry()
    return self.currentNumDisplayedEntries < self.maxDisplayedEntries
end

function ZO_FadingStationaryControlBuffer:FadeAll()
    ZO_ClearNumericallyIndexedTable(self.queue)
end

do
    local function Reset(object)
        if object.fadeLabelAndBgTimeline then
            object.fadeLabelAndBgTimeline:Stop()
        end
        if object.fadeIconTimeline then
            object.fadeIconTimeline:Stop()
        end
        object:SetHidden(true)
        object.key = nil

        if object.activeLines then
            ZO_ClearNumericallyIndexedTable(object.activeLines)
        end

        -- This isn't strictly necessary, but it keeps things clean and avoids potentially nasty bugs.
         local entry = object.entry 
         if entry then 
             entry.control = nil 
    
             if entry.header then 
                 entry.header.control = nil 
             end 
    
             local lines = object.entry.lines 
             if lines then 
                 for _, line in ipairs(lines) do
                    line.control = nil
                 end
             end
    
             object.entry = nil  
         end
    end

    local function OnLabelFadeStop(timeline, completedPlayback)
        if completedPlayback then
            local fadingControlBuffer = timeline.control.fadingControlBuffer
            fadingControlBuffer.currentlyFadingEntries = fadingControlBuffer.currentlyFadingEntries - 1
            fadingControlBuffer:RefreshBatchAfterFading() 
        end
    end

    local function OnIconFadeStop(timeline, completedPlayback)
        if completedPlayback then
            local fadingControlBuffer = timeline.control.fadingControlBuffer
            timeline.control.fadeLabelAndBgTimeline:PlayFromStart()
        end
    end

    local function InitializeItemControl(control, fadeAnimationName, iconAnimationName)
        control.icon = control:GetNamedChild("Icon")
        control.label = control:GetNamedChild("Label")
        control.bg = control:GetNamedChild("Bg")

        local fadeLabelAndBgTimeline = ANIMATION_MANAGER:CreateTimelineFromVirtual(fadeAnimationName)
        fadeLabelAndBgTimeline:GetAnimation(1):SetAnimatedControl(control.bg)
        fadeLabelAndBgTimeline:GetAnimation(2):SetAnimatedControl(control.label)
        local fadeIconTimeline = ANIMATION_MANAGER:CreateTimelineFromVirtual(iconAnimationName, control.icon)

        fadeLabelAndBgTimeline.control = control
        fadeIconTimeline.control = control
        fadeLabelAndBgTimeline:SetHandler("OnStop", OnLabelFadeStop)
        fadeIconTimeline:SetHandler("OnStop", OnIconFadeStop)

        control.fadeLabelAndBgTimeline = fadeLabelAndBgTimeline
        control.fadeIconTimeline = fadeIconTimeline

        fadeIconTimeline:Stop()
        fadeLabelAndBgTimeline:Stop()

        return control
    end

    function ZO_FadingStationaryControlBuffer:AcquireEntryObject(templateName)
        local pool = self.entryPools[templateName]
        if not pool then
            local function Factory(pool)
                local parent = self.control
                local name = string.format("%s%s%s%d", parent:GetName(), templateName, self.controllerType, pool:GetNextControlId())
                local control = CreateControl(name, parent, CT_CONTROL)
                control:SetResizeToFitDescendents(true)
                control.pool = pool
                control.activeLines = {}
                control.type = CONTROL_TYPE_ENTRY
                return control
            end

            pool = ZO_ObjectPool:New(Factory, Reset)
            self.entryPools[templateName] = pool
        end

        local control, key = pool:AcquireObject()
        control:SetHidden(false)
        control.key = key
        control.fadingControlBuffer = self
        ZO_ClearNumericallyIndexedTable(control.activeLines)

        return control
    end

    function ZO_FadingStationaryControlBuffer:AcquireItemObject(name, templateName, pools, parent, offsetY)
        local pool = pools[templateName]
        if not pool then
            local function Factory(pool)
                local name = string.format("%s%s%s", templateName, name, self.controllerType)
                local control = ZO_ObjectPool_CreateNamedControl(name, templateName, pool, pool.parent)
                control.pool = pool
                control.type = CONTROL_TYPE_ITEM

                -- Save the anchor Y offsets so we can preserve them when adjusting anchors later.
                control.anchorOffsetY = {nil, nil} -- Pre-allocate space for two anchors.
                for i = 0, MAX_ANCHORS - 1 do
                    local isValid, _, _, _, _, offsetY = control:GetAnchor(i)
                    if isValid then
                        control.anchorOffsetY[i] = offsetY
                    end
                end

                return InitializeItemControl(control, self.fadeAnimationName, self.iconAnimationName)
            end

            pool = ZO_ObjectPool:New(Factory, Reset)
            pools[templateName] = pool
        end

        -- We set the parent before acquiring the object so we can reuse the same factory function across multiple parents.
        -- To keep the item control name simple, it must always be created as a child of an entry control.
        pool.parent = parent
        local control, key = pool:AcquireObject()
        control:SetParent(parent)
        control:SetHidden(false)
        control.key = key
        control.fadingControlBuffer = self

        return control
    end

    local HEADER_INDEX = 1
    local PRESERVE_FADE = true
    function ZO_FadingStationaryControlBuffer:SetupItem(hasHeader, item, templateName, setupFn, pools, parent, offsetY, isHeader)
        if item then
            local control = self:AcquireItemObject(isHeader and "Header" or "Line", templateName, pools, parent, offsetY)
            setupFn(control, item)

            item.control = control
            control.fadeInHold = true

            local insertionIndex = HEADER_INDEX + 1
            if isHeader or not hasHeader then
                insertionIndex = HEADER_INDEX
            end

            table.insert(parent.activeLines, insertionIndex, control)
        end

        return offsetY
    end
end

function ZO_FadingStationaryControlBuffer:UpdateFadeInDelay(entryControl, fadeInDelayFactor)
    table.insert(self.queuedTimedEntries, {
                                            control = entryControl, 
                                            fadeStartDelayMs = fadeInDelayFactor + GetFrameTimeMilliseconds()
                                          }
                ) 
end

function ZO_FadingStationaryControlBuffer:DisplayEntry(templateName, entry, entryNumber, hasCurrentEntries)
    local entryControl = self:AcquireEntryObject(templateName)
    local templateData = self.templates[templateName]

    -- Call the setup function for the header and each of the lines.
    local offsetY = 0
    local HEADER_ITEM = true
    offsetY = self:SetupItem(HEADER_ITEM, entry.header, templateData.headerTemplateName, templateData.headerSetup, self.headerPools, entryControl, offsetY, HEADER_ITEM)
    local lines = entry.lines
    if lines then
        local hasHeader = (entry.header ~= nil)
        local linePools = self.linePools
        for i = #lines, 1, -1 do
            offsetY = self:SetupItem(hasHeader, lines[i], templateName, templateData.setup, linePools, entryControl, offsetY)
        end
    end

    entry.control = entryControl
    entryControl.entry = entry
    entryControl.setupTimeMS = GetFrameTimeMilliseconds()

    self.anchor:Set(entryControl)

    if hasCurrentEntries then
        if entryNumber == 0 then
            entryControl:SetAnchor(BOTTOMRIGHT, self.control, BOTTOMRIGHT, 0, 0)
            self.bottomEntry:SetAnchor(BOTTOMRIGHT, entryControl, TOPRIGHT, 0, self.additionalEntrySpacingY)
        else
            entryControl:SetAnchor(BOTTOMRIGHT, self.lastAnchoredEntry, TOPRIGHT, 0, self.additionalEntrySpacingY)
            self.bottomEntry:SetAnchor(BOTTOMRIGHT, entryControl, TOPRIGHT, 0, self.additionalEntrySpacingY)
        end
    else
        if not self.lastAnchoredEntry then
            entryControl:SetAnchor(BOTTOMRIGHT, self.control, BOTTOMRIGHT, 0, 0)
        else
            entryControl:SetAnchor(BOTTOMRIGHT, self.lastAnchoredEntry, TOPRIGHT, 0, self.additionalEntrySpacingY)
        end
    end

    table.insert(self.activeEntries, 1, entryControl)
    self.currentNumDisplayedEntries = self.currentNumDisplayedEntries + 1
    self.currentlyFadingEntries = self.currentlyFadingEntries + 1
    local subControl = entryControl:GetChild(1)
    local FADE_IN_DELAY_FACTOR = entryNumber * FADING_ENTRY_OFFSET_TIME_MS
    self:UpdateFadeInDelay(subControl, FADE_IN_DELAY_FACTOR)
    subControl.label:SetAlpha(0)
    subControl.bg:SetAlpha(0)
    subControl.icon:SetAlpha(0)
    subControl.icon:SetScale(2)
    
    self.lastAnchoredEntry = entryControl

    return entryControl
end

function ZO_FadingStationaryControlBuffer:FlushEntries()
    local hadEntries = false
    local newBatch = {}
    while #self.queue > 0 do
        local queuedEntry = self.queue[1]
        local templateName = queuedEntry.templateName
        local persistent = queuedEntry.isPersistent

        local persistHandled = false
        if persistent then
            persistHandled = self:TryConcatWithExistingEntry(templateName, queuedEntry, self.activeEntries)
        end

        if not persistHandled then
            local handled = self:TryConcatWithExistingEntry(templateName, queuedEntry, newBatch)
            if not handled then
                self:AddToBatch(templateName, queuedEntry, newBatch)
                handled = true
                hadEntries = true
            end
        else
            self.containerStartTimeMs = GetFrameTimeMilliseconds()
        end

        table.remove(self.queue, 1)
    end

    if hadEntries then
        newBatch.iterator = #newBatch
        table.insert(self.queuedBatches, newBatch)
        self:DisplayBatches()
    end

end

function ZO_FadingStationaryControlBuffer:AddToBatch(templateName, queuedEntry, batch)
    local batchData = { 
                        batch = batch,
                        templateName = templateName,
                        entry = queuedEntry, 
                       }

    table.insert(batch, 1, batchData)
end

function ZO_FadingStationaryControlBuffer:DisplayBatches()
    local noMoreEntries = false
    local displayItems = 0
    local hasCurrentEntries = self.currentNumDisplayedEntries > 0
    local latestBottomEntry = nil
    while self:CanDisplayMore() do
        local currentBatch = self.queuedBatches[1]
        if currentBatch == nil then
            break
        end
        for i = currentBatch.iterator, 1, -1 do
            if self:CanDisplayEntry() then
                local control = self:DisplayEntry(currentBatch[i].templateName, currentBatch[i].entry, displayItems, hasCurrentEntries)
                if displayItems == 0 then
                    latestBottomEntry = control
                end
                displayItems = displayItems + 1
            else
                noMoreEntries = true
                currentBatch.iterator = i
                break
            end
        end

        if noMoreEntries then
            break
        else
            table.remove(self.queuedBatches, 1)
            currentBatch = nil
        end
    end

    if displayItems > 0 then
        if latestBottomEntry then
            self.bottomEntry = latestBottomEntry
        end
        self.control:SetAlpha(1)
        self.containerStartTimeMs = GetFrameTimeMilliseconds()
        self.doesContainsEntries = true
    end
end