local pluginName, pluginShared = ...
local Lockouts = pluginShared.Lockouts

local L = LibStub("AceLocale-3.0"):GetLocale("AppearanceMatic")
local guarded = pluginShared.guarded
local reported = pluginShared.reported or guarded

local SPIN_TEXTURE = "Interface\\COMMON\\StreamCircle"
local SPIN_FALLBACK = "Interface\\AddOns\\AppearanceMatic\\Media\\Textures\\AppearanceMatic-icon.png"
local SPIN_SIZE = 48
local SPIN_SECONDS = 1.1
-- Once the cover is up it stays this long at least, so data that lands a moment later does not flash
-- it; while the data is still arriving the list is looked at again this often; and after this long it
-- is revealed whatever the data is doing, so the panel is never held hostage.
local COVER_FLOOR = 0.4
local COVER_RECHECK = 1
local COVER_CEILING = 15
-- Data that lands this soon after the open never shows the cover at all: a cover that comes and goes
-- inside a blink is a flash, and a list that fills itself in that fast never needed one.
local COVER_GRACE = 0.35
local LOG_ENTRIES = 8

-- The panel's own signal. CollectStarredDrops counts the starred looks whose drop data the game has
-- answered and the ones it is still working on. A first open with nothing cached has every look
-- pending, and that is what the cover is for; one straggler among hundreds of answered looks is not,
-- so the list shows with that row saying it is still retrieving.
function Lockouts:PanelLoading()
    local pending = self.dropsPending or 0
    return pending > 0 and pending > (self.dropsReady or 0)
end

-- What the cover takes down: everything but the frame and the spinner. The window chrome (title,
-- close, grip) stays, so the player can still close or move it.
function Lockouts:CoveredParts()
    local parts = { self.scroll, self.searchBox, self.charDropdown }
    if self.filterBoxes then
        for _, box in pairs(self.filterBoxes) do
            parts[#parts + 1] = box
        end
    end
    return parts
end

function Lockouts:EnsureLoadingCover()
    if self.loadingCover then
        return self.loadingCover
    end
    local panel = self.panel
    if not (panel and type(CreateFrame) == "function") then
        return nil
    end
    local cover = CreateFrame("Frame", nil, panel)
    cover:SetAllPoints(panel)
    cover:SetFrameStrata("DIALOG")
    cover:EnableMouse(false)
    local disc = cover:CreateTexture(nil, "ARTWORK")
    disc:SetSize(SPIN_SIZE, SPIN_SIZE)
    disc:SetPoint("CENTER", cover, "CENTER", 0, 10)
    disc:SetTexture(SPIN_TEXTURE)
    if type(disc.GetTexture) == "function" and not disc:GetTexture() then
        disc:SetTexture(SPIN_FALLBACK)
    end
    local group = disc:CreateAnimationGroup()
    local turn = group:CreateAnimation("Rotation")
    turn:SetDuration(SPIN_SECONDS)
    turn:SetDegrees(-360)
    turn:SetSmoothing("NONE")
    group:SetLooping("REPEAT")
    cover.spin = group
    local label = cover:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    label:SetPoint("TOP", disc, "BOTTOM", 0, -8)
    label:SetText(L["lock_loading"])
    cover.label = label
    cover:Hide()
    self.loadingCover = cover
    return cover
end

local function cancelTimer(self)
    if self.coverTimer then
        pcall(self.coverTimer.Cancel, self.coverTimer)
        self.coverTimer = nil
    end
end

-- One timer at a time. It does not read the flag again, it builds the list again: the flag can only
-- change inside a build, and the build ends by coming back here.
local function armTimer(self, wait)
    cancelTimer(self)
    if not (type(C_Timer) == "table" and type(C_Timer.NewTimer) == "function") then
        return
    end
    self.coverTimer = C_Timer.NewTimer(wait, function()
        Lockouts.coverTimer = nil
        if Lockouts.panel and Lockouts.panel:IsShown() and not Lockouts.minimized and Lockouts.Rebuild then
            -- through the same duty cap as every other ask, so a slow draw is not asked for every second
            if Lockouts.ScheduleRebuild then
                Lockouts:ScheduleRebuild()
            else
                Lockouts:Rebuild()
            end
        else
            Lockouts:UpdateLoadingCover()
        end
    end)
end

local function showParts(self)
    if not self.coverTookParts then
        return
    end
    self.coverTookParts = nil
    if self.minimized then
        -- minimized keeps them down itself; restoring them here would pop the list into a bar
        return
    end
    for _, part in ipairs(self:CoveredParts()) do
        reported(part.Show, part)
    end
end

local function hideParts(self)
    if self.coverTookParts then
        return
    end
    self.coverTookParts = true
    for _, part in ipairs(self:CoveredParts()) do
        reported(part.Hide, part)
    end
end

-- Takes the cover down and puts the list back. From the panel closing, the reveal is forgotten too,
-- so the next open decides for itself whether its data is there.
-- The last few decisions, for /am diag: when, what the drop scan counted, and what was done
function Lockouts:NoteCover(now, action)
    local log = self.coverLog or {}
    self.coverLog = log
    log[#log + 1] = string.format("%.1f ready=%d pending=%d %s", now, self.dropsReady or 0, self.dropsPending or 0, action)
    while #log > LOG_ENTRIES do
        table.remove(log, 1)
    end
end

function Lockouts:HideLoadingCover()
    cancelTimer(self)
    self.coverSince = nil
    self.coverShown, self.coverShownAt = nil, nil
    if self.panel and not self.panel:IsShown() then
        self.panelRevealed = nil
    end
    local cover = self.loadingCover
    if not cover then
        return
    end
    if cover.spin and cover.spin.Stop then
        reported(cover.spin.Stop, cover.spin)
    end
    cover:Hide()
    showParts(self)
end

-- Runs at the end of every build. Decides once per open: a list whose data is there is shown as it
-- is and never covered afterwards, so a background refresh never covers a list the player is reading.
-- A list still arriving is covered, looked at again while it arrives, and revealed once it is there
-- (or after the ceiling regardless).
function Lockouts:UpdateLoadingCover()
    if self.panelRevealed or self.minimized or not (self.panel and self.panel:IsShown()) then
        self:HideLoadingCover()
        return false
    end
    local now = type(GetTime) == "function" and GetTime() or 0
    local loading = self:PanelLoading()
    if not self.coverSince then
        if not loading then
            -- the common open: the data is cached, nothing is built, nothing is shown
            self:NoteCover(now, "cached, revealed")
            self.panelRevealed = true
            return false
        end
        self.coverSince = now
    end
    local waited = now - self.coverSince
    if not loading and not self.coverShown then
        -- the data landed inside the grace: the cover was never shown, so there is nothing to hold
        self:NoteCover(now, "landed in grace, revealed")
        self.panelRevealed = true
        self:HideLoadingCover()
        return false
    end
    -- the floor is measured from when the cover was shown, not from the open: a cover shown at the
    -- end of the grace and taken down a tenth of a second later would be the flash all over again
    local shownFor = self.coverShownAt and (now - self.coverShownAt) or 0
    if (not loading and shownFor >= COVER_FLOOR) or waited >= COVER_CEILING then
        self:NoteCover(now, loading and "ceiling, revealed" or "landed, revealed")
        self.panelRevealed = true
        self:HideLoadingCover()
        return false
    end
    if waited < COVER_GRACE then
        -- still arriving, but not for long enough to be sure: looked at again at the end of the grace
        self:NoteCover(now, "loading, in grace")
        armTimer(self, COVER_GRACE - waited)
        return false
    end
    local cover = self:EnsureLoadingCover()
    if not cover then
        return false
    end
    if not self.coverShown then
        self:NoteCover(now, "covered")
        self.coverShownAt = now
    end
    self.coverShown = true
    hideParts(self)
    cover:Show()
    if cover.spin and cover.spin.Play and not (cover.spin.IsPlaying and cover.spin:IsPlaying()) then
        reported(cover.spin.Play, cover.spin)
    end
    local wait = loading and COVER_RECHECK or (COVER_FLOOR - shownFor)
    local ceiling = COVER_CEILING - waited
    if wait > ceiling then
        wait = ceiling
    end
    armTimer(self, wait > 0.05 and wait or 0.05)
    return true
end
