local MAJOR, MINOR = "LibMatic-OverlayGuard-1.0", 5
assert(LibStub, MAJOR .. " requires LibStub")
local lib = LibStub:NewLibrary(MAJOR, MINOR)
if not lib then return end

lib.maxHold = lib.maxHold or 10
lib.watchTimeout = lib.watchTimeout or 20
lib.mediaTimeout = lib.mediaTimeout or 600
lib.settleFrames = lib.settleFrames or 10
lib.settleFrameTime = lib.settleFrameTime or 0.1
lib.settleTimeout = lib.settleTimeout or 5

lib._blockers = lib._blockers or {}
lib._queue = lib._queue or {}
lib._hooked = lib._hooked or {}

lib._clock = lib._clock or function()
    if _G.GetTime then return _G.GetTime() end
    return 0
end

local function now(self)
    return self._clock()
end

local function holdOf(opts)
    if type(opts) == "table" then return tonumber(opts.maxHold) end
    return nil
end

function lib:SetClock(fn)
    if type(fn) ~= "function" then return end
    local rebased = fn()
    if type(rebased) == "number" then
        for _, info in pairs(self._blockers) do info.openedAt = rebased end
        for i = 1, #self._queue do self._queue[i].at = rebased end
        if self._settleDeadline then self._settleDeadline = rebased + self.settleTimeout end
    end
    self._clock = fn
end

function lib:_activeCount()
    local n = 0
    for _ in pairs(self._blockers) do n = n + 1 end
    return n
end

function lib:_expireStaleBlockers()
    local t = now(self)
    for key, info in pairs(self._blockers) do
        if info.timeout and (t - info.openedAt) >= info.timeout then
            self._blockers[key] = nil
        end
    end
end

function lib:IsBlocked()
    if self._armedMinor ~= MINOR and _G.CreateFrame then self:_armDefaultWatches() end
    self:_expireStaleBlockers()
    return self:_activeCount() > 0
end

function lib:Block(key, opts)
    if key == nil then return end
    opts = opts or {}
    if self._blockers[key] then return end
    self._blockers[key] = { openedAt = now(self), timeout = opts.timeout }
end

function lib:_touchBlock(key, opts)
    if key == nil then return end
    opts = opts or {}
    local info = self._blockers[key]
    if info then
        info.openedAt = now(self)
        info.timeout = opts.timeout
    else
        self:Block(key, opts)
    end
end

function lib:_pruneQueue()
    local t = now(self)
    local q = self._queue
    local w = 0
    for i = 1, #q do
        local item = q[i]
        if (t - item.at) <= (item.hold or self.maxHold) then
            w = w + 1
            q[w] = item
        end
    end
    for i = #q, w + 1, -1 do q[i] = nil end
end

function lib:_flush()
    local t = now(self)
    local q = self._queue
    self._queue = {}
    local handler = _G.geterrorhandler and _G.geterrorhandler()
    for i = 1, #q do
        local item = q[i]
        if (t - item.at) <= (item.hold or self.maxHold) then
            local ok, err = pcall(item.fn)
            if not ok and handler then handler(err) end
        end
    end
end

function lib:Unblock(key)
    if key == nil or not self._blockers[key] then return end
    self._blockers[key] = nil
    self:_expireStaleBlockers()
    if self:_activeCount() == 0 then
        self:_flush()
    end
end

function lib:Poll()
    self:_expireStaleBlockers()
    self:_pruneQueue()
    if self:_activeCount() == 0 and #self._queue > 0 then
        self:_flush()
    end
end

function lib:RunWhenClear(fn, opts)
    if type(fn) ~= "function" then return end
    if self:IsBlocked() then
        self._queue[#self._queue + 1] = { fn = fn, at = now(self), hold = holdOf(opts) }
        return
    end
    if #self._queue > 0 then self:_flush() end
    if self:IsBlocked() then
        self._queue[#self._queue + 1] = { fn = fn, at = now(self), hold = holdOf(opts) }
        return
    end
    fn()
end

local function hookShowHide(self, frame, key)
    if not frame or not frame.HookScript then return end
    if frame.IsShown and frame:IsShown() then
        self:Block(key, { timeout = self.watchTimeout })
    end
    frame:HookScript("OnShow", function() self:Block(key, { timeout = self.watchTimeout }) end)
    frame:HookScript("OnHide", function() self:Unblock(key) end)
end

function lib:RegisterWatch(spec)
    if type(spec) ~= "table" then return end
    if spec.frame and spec.key then
        hookShowHide(self, spec.frame, spec.key)
    end
end

function lib:_stopRenderSettle()
    local watcher = self._watcher
    if watcher and watcher.SetScript then
        watcher:SetScript("OnUpdate", nil)
    end
    self._settleCount = nil
    self._settleKey = nil
    self._settleDeadline = nil
end

function lib:_onRenderFrame(elapsed)
    local key = self._settleKey
    if not key or not self._blockers[key] then
        self:_stopRenderSettle()
        return
    end
    if self._settleDeadline and now(self) >= self._settleDeadline then
        self:_stopRenderSettle()
        self:Unblock(key)
        return
    end
    if (elapsed or 1) <= self.settleFrameTime then
        self._settleCount = (self._settleCount or 0) + 1
    else
        self._settleCount = 0
    end
    if self._settleCount >= self.settleFrames then
        self:_stopRenderSettle()
        self:Unblock(key)
    end
end

function lib:_beginRenderSettle(key)
    if not self._blockers[key] then return end
    local watcher = self._watcher
    if not watcher or not watcher.SetScript then
        self:Unblock(key)
        return
    end
    self._settleKey = key
    self._settleCount = 0
    self._settleDeadline = now(self) + self.settleTimeout
    watcher:SetScript("OnUpdate", function(_, elapsed)
        self:_onRenderFrame(elapsed)
    end)
end

function lib:_hookDefaultFrames()
    self._hooked = self._hooked or {}
    local function tryHook(frame, key)
        if frame and frame.HookScript and not self._hooked[key] then
            self._hooked[key] = true
            hookShowHide(self, frame, key)
        end
    end
    tryHook(_G.EventToastManagerFrame, "eventtoast")
    tryHook(_G.TalkingHeadFrame, "talkinghead")
    if _G.C_MajorFactions then
        tryHook(_G.MajorFactionRenownFrame, "renown")
    end
end

function lib:_armDefaultWatches()
    if not _G.CreateFrame then return end
    if self._armedMinor == MINOR then
        self:_hookDefaultFrames()
        return
    end

    local watcher = self._watcher or _G.CreateFrame("Frame")
    watcher:RegisterEvent("CINEMATIC_START")
    watcher:RegisterEvent("CINEMATIC_STOP")
    watcher:RegisterEvent("PLAY_MOVIE")
    watcher:RegisterEvent("STOP_MOVIE")
    watcher:RegisterEvent("PLAYER_ENTERING_WORLD")
    watcher:RegisterEvent("LOADING_SCREEN_ENABLED")
    watcher:RegisterEvent("LOADING_SCREEN_DISABLED")
    watcher:RegisterEvent("ADDON_LOADED")
    watcher:SetScript("OnEvent", function(_, event)
        if event == "CINEMATIC_START" then
            self:Block("cinematic", { timeout = self.mediaTimeout })
        elseif event == "CINEMATIC_STOP" then
            self:_beginRenderSettle("cinematic")
        elseif event == "PLAY_MOVIE" then
            self:Block("movie", { timeout = self.mediaTimeout })
        elseif event == "STOP_MOVIE" then
            self:_beginRenderSettle("movie")
        elseif event == "PLAYER_ENTERING_WORLD" or event == "LOADING_SCREEN_ENABLED" then
            self:_stopRenderSettle()
            self:_touchBlock("loading", { timeout = self.watchTimeout })
        elseif event == "LOADING_SCREEN_DISABLED" then
            self:_beginRenderSettle("loading")
        elseif event == "ADDON_LOADED" then
            self:_hookDefaultFrames()
        end
    end)
    self._watcher = watcher

    self:_hookDefaultFrames()

    if _G.IsLoggedIn and not _G.IsLoggedIn() then
        self:_touchBlock("loading", { timeout = self.watchTimeout })
    end

    if _G.C_Timer and _G.C_Timer.NewTicker and not self._ticker then
        self._ticker = _G.C_Timer.NewTicker(0.5, function() self:Poll() end)
    end

    self._armedMinor = MINOR
end

local SIM_STYLES = {
    levelup = { dim = 0.45, label = "LEVEL UP", banner = "gold" },
    renown = { dim = 0.55, label = "RENOWN", banner = "renown", sub = "Level 12" },
    cinematic = { dim = 1.0, letterbox = true },
}

local function clampSimSeconds(s)
    s = tonumber(s) or 4
    if s < 1 then return 1 end
    if s > 20 then return 20 end
    return s
end

local function ensureSimFrame(self)
    if self._simFrame then return self._simFrame end
    if not _G.CreateFrame then return nil end
    local f = _G.CreateFrame("Frame", nil, _G.UIParent)
    f:SetAllPoints(_G.UIParent)
    f:SetFrameStrata("FULLSCREEN_DIALOG")
    f:SetToplevel(true)
    f:Hide()

    local dim = f:CreateTexture(nil, "BACKGROUND")
    dim:SetAllPoints(f)
    dim:SetTexture("Interface\\Buttons\\WHITE8X8")
    f.simDim = dim

    local topBar = f:CreateTexture(nil, "ARTWORK")
    topBar:SetTexture("Interface\\Buttons\\WHITE8X8")
    topBar:SetPoint("TOPLEFT", f, "TOPLEFT", 0, -110)
    topBar:SetPoint("TOPRIGHT", f, "TOPRIGHT", 0, -110)
    topBar:SetHeight(3)
    f.simTopBar = topBar

    local bottomBar = f:CreateTexture(nil, "ARTWORK")
    bottomBar:SetTexture("Interface\\Buttons\\WHITE8X8")
    bottomBar:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", 0, 110)
    bottomBar:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", 0, 110)
    bottomBar:SetHeight(3)
    f.simBottomBar = bottomBar

    local strip = f:CreateTexture(nil, "ARTWORK")
    strip:SetTexture("Interface\\Buttons\\WHITE8X8")
    strip:SetPoint("CENTER", f, "CENTER", 0, 160)
    strip:SetSize(440, 60)
    f.simStrip = strip

    local label = f:CreateFontString(nil, "OVERLAY", "GameFontNormalHuge")
    label:SetPoint("CENTER", strip, "CENTER", 0, 0)
    f.simLabel = label

    local sub = f:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    sub:SetPoint("TOP", label, "BOTTOM", 0, -8)
    f.simSub = sub

    self._simFrame = f
    return f
end

local function styleSimFrame(f, key)
    local cfg = SIM_STYLES[key] or SIM_STYLES.levelup
    f.simDim:SetVertexColor(0, 0, 0, cfg.dim)
    if cfg.letterbox then
        f.simTopBar:SetVertexColor(0.35, 0.35, 0.35, 0.85)
        f.simBottomBar:SetVertexColor(0.35, 0.35, 0.35, 0.85)
        f.simTopBar:Show()
        f.simBottomBar:Show()
    else
        f.simTopBar:Hide()
        f.simBottomBar:Hide()
    end
    if cfg.label then
        if cfg.banner == "gold" then
            f.simStrip:SetVertexColor(0.80, 0.65, 0.10, 0.85)
            f.simLabel:SetTextColor(1, 0.82, 0)
        else
            f.simStrip:SetVertexColor(0.10, 0.20, 0.35, 0.85)
            f.simLabel:SetTextColor(0.95, 0.85, 0.55)
        end
        f.simStrip:Show()
        f.simLabel:SetText(cfg.label)
        f.simLabel:Show()
        if cfg.sub then
            f.simSub:SetText(cfg.sub)
            f.simSub:Show()
        else
            f.simSub:Hide()
        end
    else
        f.simStrip:Hide()
        f.simLabel:Hide()
        f.simSub:Hide()
    end
end

function lib:SimulateOverlay(style, seconds)
    local key = "levelup"
    if type(style) == "string" then
        local s = style:lower()
        if SIM_STYLES[s] then key = s end
    end
    seconds = clampSimSeconds(seconds)

    local f = ensureSimFrame(self)
    if f then
        styleSimFrame(f, key)
        f:Show()
    end

    self:Block("simulate", { timeout = seconds + 5 })

    local function finish()
        if self._simFrame then self._simFrame:Hide() end
        self:Unblock("simulate")
    end
    if _G.C_Timer and _G.C_Timer.After then
        _G.C_Timer.After(seconds, finish)
    else
        finish()
    end
end

if _G.CreateFrame then
    lib:_armDefaultWatches()
end
