-- KiraUI-Plugin_RaidMarkers.lua
-- Raid marker panel: a movable bar of buttons that set target raid markers and
-- ground world markers, with optional group countdown and ready-check buttons.
--
-- The marker buttons are SecureActionButtonTemplate so they keep working in
-- combat. Because secure/protected frames cannot be created, re-anchored,
-- scaled, or shown/hidden during combat lockdown, every apply step is deferred
-- to PLAYER_REGEN_ENABLED while InCombatLockdown() is true. Left click = raid
-- target marker on your target; right click = world (ground) marker. The clear
-- button clears all of each.

local addonName, ns = ...
local _G = _G

local CreateFrame = CreateFrame
local C_Timer = C_Timer
local InCombatLockdown = InCombatLockdown
local GameTooltip = GameTooltip
local UIParent = UIParent
local ipairs, pairs = ipairs, pairs
local mmax = math.max

ns.hasRaidMarkers = true

----------------------------------------------------------------------
-- Settings
----------------------------------------------------------------------
local DEFAULTS = {
    enable = false,
    lock = false,
    scale = 1,
    spacing = 4,
    orientation = 'HORIZONTAL', -- HORIZONTAL or VERTICAL
    showClear = true,
    showCountdown = true,
    countdownSeconds = 10,
    showReadyCheck = true,
    groupOnly = false,        -- only show the bar while in a party/raid
    x = 0,
    y = -200,
}

local function GetCfg() return ns.ModuleCfg('raidMarkers', DEFAULTS) end
ns.RMGetCfg = GetCfg


-- Raid-target icon index (1=star .. 8=skull) -> ground world-marker index.
-- PlaceRaidMarker does NOT use the same index order as the raid-target icons, so
-- we remap via the standard WORLD_RAID_MARKER_ORDER (the same table Blizzard
-- use) so the star button drops the star-coloured ground marker, etc.
local WORLD_MARKER_ORDER = { [1] = 8, [2] = 4, [3] = 1, [4] = 7, [5] = 2, [6] = 3, [7] = 6, [8] = 5 }
local function WorldMarkerID(iconMarker)
    -- The marker buttons are laid out 1->8 (star..skull). The world-marker (beam)
    -- index that COLOUR-matches each raid-target icon is WORLD_MARKER_ORDER[9-icon]
    -- net mapping: star->5(yellow) circle->6(orange)
    -- diamond->3(purple) triangle->2(green) moon->7(silver) square->1(blue)
    -- cross->4(red) skull->8(white).
    return WORLD_MARKER_ORDER[9 - iconMarker] or iconMarker
end

local BTN = 28           -- base button size (panel scale multiplies this)
local PAD = 8            -- panel padding around the button row

----------------------------------------------------------------------
-- Frame + buttons
----------------------------------------------------------------------
local panel
local markerButtons = {}
local clearButton, countdownButton, readyCheckButton
local pullActive        -- a countdown we started and have not seen finish
local previewActive
local pendingApply = false

local function InGroup()
    return (_G.IsInGroup and _G.IsInGroup()) or (_G.IsInRaid and _G.IsInRaid())
end

-- IMPORTANT (taint): every visual lives on a NON-secure child frame (visualRoot),
-- never directly on the SecureActionButton. Creating/altering a secure frame's own
-- regions from addon code marks the secure frame as addon-tainted; a tainted
-- "raidtarget" click then runs SetRaidTarget in a tainted context and leaves that
-- protected function globally tainted, which blocks the player's own /tm macro.
-- Keeping the secure button pristine (only SetSize / RegisterForClicks /
-- SetAttribute / hover scripts) is what avoids
-- the "AddOn tried to call SetRaidTarget()" forbidden error.
local function NewVisualRoot(b)
    local v = CreateFrame('Frame', nil, b)
    v:SetAllPoints(b)
    v:EnableMouse(false)
    b.visualRoot = v
    return v
end

-- Secure clicks: WHICH edge performs the action depends on the client build
-- and the ActionButtonUseKeyDown CVar (live 12.0 forces mouse to the UP edge;
-- 12.1 lets mouse honor the CVar). SecureActionButton_OnClick itself gates
-- the action to exactly ONE edge (its clickAction check), so registering
-- BOTH edges is correct on every build: the active edge acts, the other is
-- ignored by the template - no double-fire, works with any CVar state.
local function MakeMarkerButton(marker)
    local b = CreateFrame('Button', nil, panel, 'SecureActionButtonTemplate')
    b:SetSize(BTN, BTN)
    b:RegisterForClicks('AnyDown', 'AnyUp')
    b.marker = marker

    local v = NewVisualRoot(b)

    b.bg = v:CreateTexture(nil, 'BACKGROUND')
    b.bg:SetAllPoints()
    b.bg:SetColorTexture(0, 0, 0, 0.85)

    b.icon = v:CreateTexture(nil, 'ARTWORK')
    b.icon:SetPoint('TOPLEFT', v, 'TOPLEFT', 1, -1)
    b.icon:SetPoint('BOTTOMRIGHT', v, 'BOTTOMRIGHT', -1, 1)
    b.icon:SetTexture('Interface\\TargetingFrame\\UI-RaidTargetingIcons')
    if _G.SetRaidTargetIconTexture then _G.SetRaidTargetIconTexture(b.icon, marker) end

    -- Hover glow (toggled from OnEnter/OnLeave since visualRoot takes no mouse).
    b.hl = v:CreateTexture(nil, 'OVERLAY')
    b.hl:SetAllPoints(v)
    b.hl:SetTexture('Interface\\TargetingFrame\\UI-RaidTargetingIcons')
    b.hl:SetBlendMode('ADD')
    b.hl:SetAlpha(0)
    if _G.SetRaidTargetIconTexture then _G.SetRaidTargetIconTexture(b.hl, marker) end

    -- Secure attributes (set once, out of combat): left = raid target, right = world marker.
    b:SetAttribute('type1', 'raidtarget')
    b:SetAttribute('action1', 'toggle')
    b:SetAttribute('marker1', marker)
    b:SetAttribute('unit1', 'target')
    b:SetAttribute('type2', 'worldmarker')
    b:SetAttribute('action2', 'toggle')
    b:SetAttribute('marker2', WorldMarkerID(marker))

    b:SetScript('OnEnter', function(self)
        if self.hl then self.hl:SetAlpha(0.25) end
        GameTooltip:SetOwner(self, 'ANCHOR_TOP')
        GameTooltip:AddLine('Left: Mark Target', 1, 1, 1)
        GameTooltip:AddLine('Right: World Marker', 1, 1, 1)
        GameTooltip:Show()
    end)
    b:SetScript('OnLeave', function(self)
        if self.hl then self.hl:SetAlpha(0) end
        GameTooltip:Hide()
    end)
    return b
end

-- iconScale lets the glyph overflow the button so atlases with transparent
-- padding (the countdown / ready-check GM icons) render at a usable size.
-- Visuals live on visualRoot (see MakeMarkerButton) to keep the secure button clean.
local function MakeAtlasButton(atlas, iconScale, fallbackR, fallbackG, fallbackB)
    local b = CreateFrame('Button', nil, panel, 'SecureActionButtonTemplate')
    b:SetSize(BTN, BTN)
    b:RegisterForClicks('AnyDown', 'AnyUp')  -- see MakeMarkerButton: both edges, template gates

    local v = NewVisualRoot(b)

    b.bg = v:CreateTexture(nil, 'BACKGROUND')
    b.bg:SetAllPoints()
    b.bg:SetColorTexture(0, 0, 0, 0.85)

    b.icon = v:CreateTexture(nil, 'ARTWORK')
    b.icon:SetPoint('CENTER')
    b.icon:SetSize(BTN * (iconScale or 1), BTN * (iconScale or 1))
    -- SetAtlas returns true when the atlas exists; fall back to a tinted square
    -- on clients that don't have it.
    local ok = atlas and b.icon.SetAtlas and b.icon:SetAtlas(atlas, false)
    if not ok then
        b.icon:SetColorTexture(fallbackR or 1, fallbackG or 1, fallbackB or 1, 0.9)
        b.icon:SetSize(BTN - 10, BTN - 10)
    end

    b.hl = v:CreateTexture(nil, 'OVERLAY')
    b.hl:SetAllPoints(v)
    b.hl:SetColorTexture(1, 1, 1, 0)
    return b
end

-- A bare PLAIN button (dark cell + hover) we decorate with custom art below.
-- Countdown / ready-check call non-protected APIs (C_PartyInfo), so these must NOT
-- be SecureActionButtonTemplate: an insecure OnClick on a secure button taints the
-- shared secure path and gets the player's own /tm SetRaidTarget macro blocked.
local function MakeBlankSecureButton()
    local b = CreateFrame('Button', nil, panel)
    b:SetSize(BTN, BTN)
    b:RegisterForClicks('AnyUp')
    b.bg = b:CreateTexture(nil, 'BACKGROUND')
    b.bg:SetAllPoints()
    b.bg:SetColorTexture(0, 0, 0, 0.85)
    b.hl = b:CreateTexture(nil, 'HIGHLIGHT')
    b.hl:SetAllPoints()
    b.hl:SetColorTexture(1, 1, 1, 0.15)
    return b
end

-- Pull timer: the classic stopwatch icon, cropped like the marker buttons.
local function DecorateClock(b)
    local t = b:CreateTexture(nil, 'ARTWORK')
    t:SetTexture('Interface\\Icons\\INV_Misc_PocketWatch_01')
    t:SetTexCoord(0.08, 0.92, 0.08, 0.92)
    t:SetAllPoints()
end

-- Ready check: Blizzard's own ready-check art (the green check coin the
-- raid frames use) - instantly recognizable.
local function DecorateReadyCheck(b)
    local t = b:CreateTexture(nil, 'ARTWORK')
    t:SetTexture('Interface\\RaidFrame\\ReadyCheck-Ready')
    t:SetSize(BTN + 4, BTN + 4)  -- circular art with built-in padding
    t:SetPoint('CENTER')
end

local function EnsurePanel()
    if panel then return end

    panel = CreateFrame('Frame', 'KiraUIRaidMarkers', UIParent)
    panel:SetFrameStrata('MEDIUM')
    panel:SetClampedToScreen(true)
    panel:SetMovable(true)
    panel:EnableMouse(true)
    panel:RegisterForDrag('RightButton')
    -- protected frame: it may not be moved while in combat
    ns.MakeDraggable(panel, GetCfg, 'x', 'y', nil,
        function(cfg) return not cfg.lock and not InCombatLockdown() end)

    for marker = 1, 8 do
        markerButtons[marker] = MakeMarkerButton(marker)
    end

    -- Clear button: left clears all raid target marks, right clears world markers.
    clearButton = MakeAtlasButton('GM-raidMarker-remove', 1.3, 1, 0.3, 0.3)
    clearButton:SetAttribute('type1', 'raidtarget')
    clearButton:SetAttribute('action1', 'clear-all')
    clearButton:SetAttribute('type2', 'worldmarker')
    clearButton:SetAttribute('action2', 'clear')
    clearButton:SetScript('OnEnter', function(self)
        if self.hl then self.hl:SetColorTexture(1, 1, 1, 0.15) end
        GameTooltip:SetOwner(self, 'ANCHOR_TOP')
        GameTooltip:AddLine('Clear', 1, 0.3, 0.3)
        GameTooltip:AddLine('Left: clear all target markers', 1, 1, 1)
        GameTooltip:AddLine('Right: clear all world markers', 0.75, 1, 0.75)
        GameTooltip:Show()
    end)
    clearButton:SetScript('OnLeave', function(self)
        if self.hl then self.hl:SetColorTexture(1, 1, 1, 0) end
        GameTooltip:Hide()
    end)

    -- Countdown + ready check are insecure (they just call C_PartyInfo) and use
    -- our own custom coin icons instead of Blizzard's GM atlases.
    countdownButton = MakeBlankSecureButton()
    DecorateClock(countdownButton)
    countdownButton:SetScript('OnClick', function(_, mouseButton)
        if not InGroup() then ns.Msg('Countdown only works in a party/raid.') return end
        -- Left-click TOGGLES. A pull already counting down is far more likely to
        -- need cancelling than restarting - and restarting it silently on a
        -- second click is the worst of the three possible behaviours.
        if mouseButton ~= 'RightButton' and pullActive then
            local pi2 = _G.C_PartyInfo
            if pi2 and pi2.DoCountdown then pcall(pi2.DoCountdown, 0) end
            pullActive = nil
            ns.Msg('Pull timer cancelled.')
            return
        end
        local pi = _G.C_PartyInfo
        if not (pi and pi.DoCountdown) then ns.Msg('Countdown API unavailable.') return end
        local secs = mouseButton == 'RightButton' and 0 or (GetCfg().countdownSeconds or 10)
        pcall(pi.DoCountdown, secs)
        pullActive = (secs > 0) or nil
        if pullActive then
            -- self-clearing, so a pull that finishes normally leaves the next
            -- click starting a fresh one rather than cancelling nothing
            _G.C_Timer.After(secs + 0.5, function()
                if pullActive then pullActive = nil end
            end)
        end
    end)
    countdownButton:SetScript('OnEnter', function(self)
        GameTooltip:SetOwner(self, 'ANCHOR_TOP')
        GameTooltip:AddLine('Countdown', 1, 0.82, 0)
        if pullActive then
            GameTooltip:AddLine('Left: CANCEL the running pull', 1, 0.4, 0.4)
        else
            GameTooltip:AddLine('Left: start ' .. (GetCfg().countdownSeconds or 10) .. 's countdown', 1, 1, 1)
        end
        GameTooltip:AddLine('Right: cancel countdown', 0.75, 1, 0.75)
        GameTooltip:Show()
    end)
    countdownButton:SetScript('OnLeave', function() GameTooltip:Hide() end)

    readyCheckButton = MakeBlankSecureButton()
    DecorateReadyCheck(readyCheckButton)
    readyCheckButton:SetScript('OnClick', function()
        if not InGroup() then ns.Msg('Ready check only works in a party/raid.') return end
        -- DoReadyCheck is a RESTRICTED function (HasRestrictions in the API
        -- docs). Calling it while the client is locked down does not fail
        -- quietly - it raises ADDON_ACTION_BLOCKED and names this addon. There
        -- is no /readycheck slash command, so a secure macro button is not an
        -- option either; the only correct move is to refuse early and say why.
        if InCombatLockdown() then
            ns.Msg('Ready check is blocked during combat.')
            return
        end
        local ci = _G.C_ChatInfo
        if ci and ci.InChatMessagingLockdown and ci.InChatMessagingLockdown() then
            ns.Msg('Ready check is blocked right now (chat lockdown).')
            return
        end
        local pi = _G.C_PartyInfo
        if pi and pi.DoReadyCheck then
            local ok = pcall(pi.DoReadyCheck)
            if not ok then ns.Msg('Ready check was blocked by the client.') end
        else
            ns.Msg('Ready check API unavailable.')
        end
    end)
    readyCheckButton:SetScript('OnEnter', function(self)
        GameTooltip:SetOwner(self, 'ANCHOR_TOP')
        GameTooltip:AddLine('Ready Check', 1, 0.82, 0)
        GameTooltip:AddLine('Left: start ready check', 1, 1, 1)
        GameTooltip:Show()
    end)
    readyCheckButton:SetScript('OnLeave', function() GameTooltip:Hide() end)

    panel:Hide()
end

-- Ordered list of the buttons currently shown, honouring the cfg toggles.
local function OrderedButtons()
    local cfg = GetCfg()
    local list = {}
    for marker = 1, 8 do list[#list + 1] = markerButtons[marker] end
    if cfg.showClear then list[#list + 1] = clearButton end
    if cfg.showCountdown then list[#list + 1] = countdownButton end
    if cfg.showReadyCheck then list[#list + 1] = readyCheckButton end
    return list
end

local function Layout()
    local cfg = GetCfg()
    local spacing = cfg.spacing or 4
    local vertical = cfg.orientation == 'VERTICAL'
    local shown = OrderedButtons()

    -- Hide the optional buttons that are toggled off.
    clearButton:SetShown(cfg.showClear and true or false)
    countdownButton:SetShown(cfg.showCountdown and true or false)
    readyCheckButton:SetShown(cfg.showReadyCheck and true or false)

    -- Each icon's 1px border overhangs by 1px per side, so its visible width is
    -- BTN + 2. Step by that plus the gap so the spacing value is the literal pixel
    -- gap you see between borders (spacing 1 = 1px), not 2px narrower.
    local n = #shown
    local step = BTN + 2 + spacing
    for i, b in ipairs(shown) do
        b:ClearAllPoints()
        if vertical then
            b:SetPoint('TOP', panel, 'TOP', 0, -PAD - (i - 1) * step)
        else
            b:SetPoint('LEFT', panel, 'LEFT', PAD + (i - 1) * step, 0)
        end
    end

    local content = BTN + mmax(0, n - 1) * step
    if vertical then
        panel:SetSize(PAD * 2 + BTN, PAD * 2 + content)
    else
        panel:SetSize(PAD * 2 + content, PAD * 2 + BTN)
    end
end

local function ApplyNow()
    EnsurePanel()
    local cfg = GetCfg()
    Layout()
    panel:SetScale(cfg.scale or 1)
    panel:ClearAllPoints()
    panel:SetPoint('CENTER', UIParent, 'CENTER', cfg.x or 0, cfg.y or -200)
    -- Preview ignores BOTH gates: the bar is normally hidden when the feature is
    -- off and when you are solo, and you need to see it to place it in either.
    local show = previewActive
        or (cfg.enable and (not cfg.groupOnly or InGroup()) and true or false)
    panel:SetShown(show and true or false)
end

-- Public apply: defer everything while in combat. Secure frames cannot be
-- created, re-anchored, scaled, or shown/hidden in combat lockdown, so we don't
-- even build the panel until we are out of combat.
local function Apply()
    if InCombatLockdown() then
        pendingApply = true
        return
    end
    pendingApply = false
    -- When the feature is OFF, do not create the secure marker buttons at all.
    -- This makes the feature cleanly testable as a taint source and avoids
    -- building secure frames nobody asked for.
    if not GetCfg().enable and not previewActive then
        if panel then panel:Hide() end
        return
    end
    EnsurePanel()
    ApplyNow()
end
ns.RMApply = function() C_Timer.After(0, Apply) end

ns.RMIsPreview = function() return previewActive and true or false end
ns.RMSetPreview = function(state)
    previewActive = state and true or false
    ns.RMApply()
end

----------------------------------------------------------------------
-- Events
----------------------------------------------------------------------
local evt = CreateFrame('Frame')
evt:RegisterEvent('PLAYER_ENTERING_WORLD')
evt:RegisterEvent('PLAYER_REGEN_ENABLED')
evt:RegisterEvent('GROUP_ROSTER_UPDATE')
evt:SetScript('OnEvent', function(_, event)
    if event == 'PLAYER_REGEN_ENABLED' then
        if pendingApply then Apply() end
    elseif event == 'GROUP_ROSTER_UPDATE' then
        Apply() -- re-evaluate "only in group" (deferred if in combat)
    else
        C_Timer.After(0.3, Apply)
    end
end)

-- Combat means the pull happened; clear the toggle so the next click starts a
-- new countdown rather than trying to cancel one that is already over.
local pullEvt = CreateFrame('Frame')
pullEvt:RegisterEvent('PLAYER_REGEN_DISABLED')
pullEvt:SetScript('OnEvent', function() pullActive = nil end)
