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

lib.frames = lib.frames or {}
lib.owners = lib.owners or setmetatable({}, { __mode = "k" })
lib.addons = lib.addons or {}
lib.position = lib.position or { x = 50, y = 70, rev = 0 }
lib.position.maxAlerts = lib.position.maxAlerts or 5

local SPACING = 15
local DEFAULT_X, DEFAULT_Y = 50, 70
local DEFAULT_MAX_ALERTS = 5
local MAX_ALERTS_LIMIT = 10

local function clampPercent(value)
    value = tonumber(value)
    if not value then return nil end
    if value < 0 then return 0 end
    if value > 100 then return 100 end
    return value
end

local function clampMaxAlerts(value)
    value = tonumber(value)
    if not value then return nil end
    value = math.floor(value + 0.5)
    if value < 1 then return 1 end
    if value > MAX_ALERTS_LIMIT then return MAX_ALERTS_LIMIT end
    return value
end

function lib:Reanchor()
    local first = self.frames[1]
    if not first then return end
    local p = self.position
    local xOffset = (p.x - 50) * UIParent:GetWidth() / 100
    local yOffset = (p.y - 50) * UIParent:GetHeight() / 100
    first:ClearAllPoints()
    first:SetPoint("CENTER", UIParent, "CENTER", xOffset, yOffset)
    for i = 2, #self.frames do
        self.frames[i]:ClearAllPoints()
        self.frames[i]:SetPoint("TOP", self.frames[i - 1], "BOTTOM", 0, -SPACING)
    end
end

function lib:GetPosition()
    return self.position.x, self.position.y
end

function lib:GetDefaultPosition()
    return DEFAULT_X, DEFAULT_Y
end

function lib:GetMaxAlerts()
    return self.position.maxAlerts
end

function lib:GetDefaultMaxAlerts()
    return DEFAULT_MAX_ALERTS
end

local function addonEnabled(opts)
    if not opts.isEnabled then return true end
    return opts.isEnabled() and true or false
end

local function broadcast(self)
    local p = self.position
    for _, opts in pairs(self.addons) do
        if opts.setSaved and addonEnabled(opts) then
            opts.setSaved(p.x, p.y, p.rev, p.maxAlerts)
        end
    end
end

local function adoptSaved(self, saved)
    if type(saved) ~= "table" then return false end
    local x = clampPercent(saved.x)
    local y = clampPercent(saved.y)
    local rev = tonumber(saved.rev)
    if not x or not y or not rev then return false end
    if rev <= self.position.rev then return false end
    self.position.x, self.position.y, self.position.rev = x, y, rev
    local maxAlerts = clampMaxAlerts(saved.maxAlerts)
    if maxAlerts then
        self.position.maxAlerts = maxAlerts
    end
    return true
end

local function bumpRev(p)
    local now = time()
    p.rev = (p.rev >= now) and (p.rev + 1) or now
end

local function enforceGlobalCap(self)
    local cap = self.position.maxAlerts
    if not cap or #self.frames <= cap then return end
    local victims = {}
    for i = #self.frames, cap + 1, -1 do
        victims[#victims + 1] = self.frames[i]
    end
    for i = 1, #victims do
        victims[i]:Hide()
    end
end

function lib:SetPosition(x, y)
    x = clampPercent(x)
    y = clampPercent(y)
    if not x or not y then return end
    local p = self.position
    p.x, p.y = x, y
    bumpRev(p)
    self:Reanchor()
    broadcast(self)
end

function lib:SetMaxAlerts(value)
    value = clampMaxAlerts(value)
    if not value then return end
    local p = self.position
    p.maxAlerts = value
    bumpRev(p)
    enforceGlobalCap(self)
    self:Reanchor()
    broadcast(self)
end

function lib:RegisterAddon(name, opts)
    if type(name) ~= "string" or type(opts) ~= "table" then return end
    self.addons[name] = opts
    if addonEnabled(opts) and opts.getSaved and adoptSaved(self, opts.getSaved()) then
        self:Reanchor()
    end
    broadcast(self)
end

function lib:SyncFromSaved(name)
    local opts = self.addons[name]
    if not opts or not opts.getSaved then return end
    if addonEnabled(opts) and adoptSaved(self, opts.getSaved()) then
        self:Reanchor()
    end
    broadcast(self)
end

function lib:Remove(frame)
    for i = #self.frames, 1, -1 do
        if self.frames[i] == frame then
            table.remove(self.frames, i)
            self.owners[frame] = nil
            self:Reanchor()
            return
        end
    end
end

function lib:Push(name, frame, maxCount)
    if not frame then return end
    for i = #self.frames, 1, -1 do
        if self.frames[i] == frame then
            table.remove(self.frames, i)
        end
    end
    table.insert(self.frames, 1, frame)
    self.owners[frame] = name
    if not frame.__lmAlertStackHooked then
        frame.__lmAlertStackHooked = true
        frame:HookScript("OnHide", function(f)
            -- OnHide also fires when an ancestor hides (a cinematic hiding
            -- UIParent), while this frame's own shown flag stays true. Only
            -- drop it from the stack on a real Hide() of the frame itself,
            -- or it goes untracked while still effectively visible: not
            -- reanchored when the stack reflows, not counted against
            -- maxCount, a stale-position ghost until it fades on its own.
            if f:IsShown() then return end
            lib:Remove(f)
        end)
    end
    maxCount = tonumber(maxCount)
    if maxCount and maxCount > 0 then
        local owned = 0
        for i = 1, #self.frames do
            if self.owners[self.frames[i]] == name then
                owned = owned + 1
            end
        end
        if owned > maxCount then
            local victims = {}
            local overflow = owned - maxCount
            for i = #self.frames, 1, -1 do
                if overflow <= 0 then break end
                local candidate = self.frames[i]
                if self.owners[candidate] == name then
                    victims[#victims + 1] = candidate
                    overflow = overflow - 1
                end
            end
            for i = 1, #victims do
                victims[i]:Hide()
            end
        end
    end
    enforceGlobalCap(self)
    self:Reanchor()
end

function lib:Detach(name)
    local detached = {}
    for i = #self.frames, 1, -1 do
        local f = self.frames[i]
        if self.owners[f] == name then
            table.remove(self.frames, i)
            self.owners[f] = nil
            detached[#detached + 1] = f
        end
    end
    for i = 1, #detached do
        local f = detached[i]
        local cx, cy = f:GetCenter()
        if cx and cy then
            f:ClearAllPoints()
            f:SetPoint("CENTER", UIParent, "BOTTOMLEFT", cx, cy)
        end
    end
    self:Reanchor()
end

function lib:Demo()
    local list = {}
    for _, opts in pairs(self.addons) do
        if type(opts.demo) == "function" and addonEnabled(opts) then
            list[#list + 1] = opts.demo
        end
    end
    for i = #list, 2, -1 do
        local j = math.random(i)
        list[i], list[j] = list[j], list[i]
    end
    for i = 1, #list do
        list[i]()
    end
    return #list
end

lib.eventFrame = lib.eventFrame or CreateFrame("Frame")
lib.eventFrame:RegisterEvent("UI_SCALE_CHANGED")
lib.eventFrame:RegisterEvent("DISPLAY_SIZE_CHANGED")
lib.eventFrame:SetScript("OnEvent", function()
    lib:Reanchor()
end)
