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

local CreateFrame = CreateFrame
local UIParent = UIParent
local C_Timer = C_Timer
local UISpecialFrames = UISpecialFrames
local tinsert = table.insert
local fastrandom = _G.fastrandom or math.random

local MOCK_INTERVAL = 1.6
local DEFAULT_PCT = 50

local SPELLS = {
    "Fireball", "Mortal Strike", "Lightning Bolt", "Shadow Bolt",
    "Frostbolt", "Sinister Strike", "Smite", "Wrath", "Pyroblast", "Chaos Bolt",
}

local STRINGS = {
    title = "Drag to position alerts",
    accept = "Accept",
    cancel = "Cancel",
    reset = "Reset",
}

local overlay, mockText, mockBounce, mockFade, mockTimer, session
local acceptButton, cancelButton, resetButton
local stopMockLoop, startMockLoop, playMock, placeOverlay, ensureOverlay

local function clampPercent(value)
    if type(value) ~= "number" then return DEFAULT_PCT end
    if value < 0 then return 0 end
    if value > 100 then return 100 end
    return value
end

local function percentToOffset(xPct, yPct)
    local width = UIParent:GetWidth()
    local height = UIParent:GetHeight()
    return (xPct - 50) / 100 * width, (yPct - 50) / 100 * height
end

local function overlayPercent()
    local fx, fy = overlay:GetCenter()
    local px, py = UIParent:GetCenter()
    if not fx or not px then return DEFAULT_PCT, DEFAULT_PCT end
    local width = UIParent:GetWidth()
    local height = UIParent:GetHeight()
    return clampPercent(50 + (fx - px) / width * 100),
        clampPercent(50 + (fy - py) / height * 100)
end

local function groupNumber(amount)
    local reversed = tostring(amount):reverse()
    local grouped = (reversed:gsub("(%d%d%d)", "%1,")):reverse()
    if grouped:sub(1, 1) == "," then
        grouped = grouped:sub(2)
    end
    return grouped
end

local function randomMockText()
    local spell = SPELLS[fastrandom(1, #SPELLS)]
    return "New " .. spell .. " Crit: " .. groupNumber(fastrandom(5000, 99999)) .. "!"
end

local function attachBounce(target)
    local ctb = LibStub("LibMatic-CombatTextBounce-1.0", true)
    if ctb and ctb.AttachBounce then
        local group = ctb:AttachBounce(target, { style = "mop" })
        if group then return group end
    end
    local group = target:CreateAnimationGroup()
    local up = group:CreateAnimation("Scale")
    up:SetScale(2.0, 2.0)
    up:SetDuration(0.05)
    up:SetOrder(1)
    local down = group:CreateAnimation("Scale")
    down:SetScale(0.5, 0.5)
    down:SetDuration(0.15)
    down:SetOrder(2)
    return group
end

function stopMockLoop()
    if mockTimer then
        mockTimer:Cancel()
        mockTimer = nil
    end
    if mockBounce then mockBounce:Stop() end
    if mockFade then mockFade:Stop() end
    if mockText then mockText:Hide() end
end

function playMock()
    if not overlay or not overlay:IsShown() then return end
    mockText:SetText(randomMockText())
    if session and session.font then
        local path, size, outline = session.font()
        if path then
            mockText:SetFont(path, size or 25, outline or "OUTLINE")
        else
            mockText:SetFontObject(GameFontNormalHuge)
        end
    else
        mockText:SetFontObject(GameFontNormalHuge)
    end
    if session and session.color then
        local r, g, b = session.color()
        if type(r) == "number" and type(g) == "number" and type(b) == "number" then
            mockText:SetTextColor(r, g, b)
        else
            mockText:SetTextColor(1, 0.1, 0.1)
        end
    else
        mockText:SetTextColor(1, 0.1, 0.1)
    end
    mockText:SetAlpha(1)
    mockText:Show()
    if mockBounce then
        mockBounce:Stop()
        mockBounce:Play()
    end
    if mockFade then
        mockFade:Stop()
        mockFade:Play()
    end
end

function startMockLoop()
    stopMockLoop()
    playMock()
    if C_Timer and C_Timer.NewTicker then
        mockTimer = C_Timer.NewTicker(MOCK_INTERVAL, playMock)
    end
end

function placeOverlay(xPct, yPct)
    local xOffset, yOffset = percentToOffset(xPct, yPct)
    overlay:ClearAllPoints()
    overlay:SetPoint("CENTER", UIParent, "CENTER", xOffset, yOffset)
end

function ensureOverlay()
    if overlay then return end

    overlay = CreateFrame("Frame", "LibMaticAlertPositionerOverlay", UIParent, "BackdropTemplate")
    overlay:SetSize(360, 96)
    overlay:SetFrameStrata("FULLSCREEN_DIALOG")
    overlay:SetToplevel(true)
    overlay:SetClampedToScreen(true)
    overlay:SetMovable(true)
    overlay:EnableMouse(true)
    overlay:RegisterForDrag("LeftButton")
    overlay:SetScript("OnShow", function(self)
        self:SetScript("OnShow", nil)
        if not self.SetBackdrop then return end
        pcall(function()
            self:SetBackdrop({
                bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
                edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
                edgeSize = 14,
                insets = { left = 4, right = 4, top = 4, bottom = 4 },
            })
            self:SetBackdropColor(0, 0, 0, 0.55)
        end)
    end)
    overlay:SetScript("OnDragStart", overlay.StartMoving)
    overlay:SetScript("OnDragStop", overlay.StopMovingOrSizing)
    overlay:SetScript("OnHide", function()
        stopMockLoop()
        local active = session
        session = nil
        if active and active.reopen then
            active.reopen()
        end
    end)
    overlay:Hide()

    local title = overlay:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    title:SetPoint("TOP", overlay, "TOP", 0, -8)
    title:SetText(STRINGS.title)

    mockText = overlay:CreateFontString(nil, "OVERLAY", "GameFontNormalHuge")
    mockText:SetPoint("CENTER", overlay, "CENTER", 0, 2)
    mockText:SetTextColor(1, 0.1, 0.1)
    mockText:Hide()

    mockBounce = attachBounce(mockText)

    mockFade = mockText:CreateAnimationGroup()
    local fade = mockFade:CreateAnimation("Alpha")
    fade:SetFromAlpha(1)
    fade:SetToAlpha(0)
    fade:SetDuration(0.8)
    fade:SetStartDelay(0.7)
    mockFade:SetScript("OnFinished", function()
        mockText:SetAlpha(0)
    end)

    if UISpecialFrames then
        tinsert(UISpecialFrames, "LibMaticAlertPositionerOverlay")
    end

    cancelButton = CreateFrame("Button", nil, overlay, "UIPanelButtonTemplate")
    cancelButton:SetSize(104, 22)
    cancelButton:SetPoint("TOP", overlay, "BOTTOM", 0, -8)
    cancelButton:SetText(STRINGS.cancel)
    cancelButton:SetScript("OnClick", function()
        overlay:Hide()
    end)

    acceptButton = CreateFrame("Button", nil, overlay, "UIPanelButtonTemplate")
    acceptButton:SetSize(104, 22)
    acceptButton:SetPoint("RIGHT", cancelButton, "LEFT", -8, 0)
    acceptButton:SetText(STRINGS.accept)
    acceptButton:SetScript("OnClick", function()
        local active = session
        if active and active.set then
            active.set(overlayPercent())
        end
        overlay:Hide()
    end)

    resetButton = CreateFrame("Button", nil, overlay, "UIPanelButtonTemplate")
    resetButton:SetSize(104, 22)
    resetButton:SetPoint("LEFT", cancelButton, "RIGHT", 8, 0)
    resetButton:SetText(STRINGS.reset)
    resetButton:SetScript("OnClick", function()
        local active = session
        local xPct, yPct = DEFAULT_PCT, DEFAULT_PCT
        if active and active.default then
            local dx, dy = active.default()
            xPct, yPct = clampPercent(dx), clampPercent(dy)
        end
        placeOverlay(xPct, yPct)
    end)
end

function lib:Open(opts)
    opts = opts or {}
    ensureOverlay()
    -- Point the live session at the most recent caller. If the overlay is
    -- already open for another addon, this makes Accept commit to whoever
    -- opened last instead of stranding the drag in the first addon's callbacks.
    session = {
        set = opts.set,
        default = opts.default,
        reopen = opts.reopen,
        font = opts.font,
        color = opts.color,
    }
    if overlay:IsShown() then
        overlay:Raise()
        return
    end
    local xPct, yPct = DEFAULT_PCT, DEFAULT_PCT
    local gx, gy
    if opts.get then
        gx, gy = opts.get()
    end
    if type(gx) == "number" and type(gy) == "number" then
        xPct, yPct = clampPercent(gx), clampPercent(gy)
    elseif opts.default then
        local dx, dy = opts.default()
        if type(dx) == "number" and type(dy) == "number" then
            xPct, yPct = clampPercent(dx), clampPercent(dy)
        end
    end
    placeOverlay(xPct, yPct)
    overlay:Show()
    startMockLoop()
end
