-- ============================================================================
-- pom/pvp/pvp.lua
-- PvP visuals (12.0 secret-safe)
--
-- Fixes:
--  - Adds P:IsActive(), so pom_mixins.lua can actually disable PvE/threat coloring.
--  - Makes open-world PvP enemy player detection more reliable.
--  - Stops PvP class color from being aggressively restored back to PvE/mob color.
--  - Keeps class color as final override when PvP module is active.
-- ============================================================================

local ADDON_NAME, ns = ...

ns = _G[ADDON_NAME] or ns
_G[ADDON_NAME] = ns

ns.pvp = ns.pvp or {}
local P = ns.pvp

local DB  = ns.pvp_db
local U   = ns.util
local CUA = C_UnitAuras

local RAID_CLASS_COLORS = RAID_CLASS_COLORS
local CUSTOM_CLASS_COLORS = CUSTOM_CLASS_COLORS

local pcall = pcall
local abs = math.abs

-- ------------------------------------------------------------
-- Instance checks
-- ------------------------------------------------------------
local function GetInstType()
    local inInstance, instType = IsInInstance()

    if not inInstance then
        return "world"
    end

    if instType == "arena" then
        return "arena"
    end

    if instType == "pvp" then
        return "bg"
    end

    return "other"
end

local function ShouldRun(dbpvp)
    if not (dbpvp and dbpvp.enabled) then
        return false
    end

    local t = GetInstType()

    if t == "arena" then
        return dbpvp.useArena and true or false
    end

    if t == "bg" then
        return dbpvp.useBG and true or false
    end

    if t == "world" then
        return dbpvp.useWorld and true or false
    end

    return false
end

function P:IsActive()
    if not (DB and DB.Get) then
        return false
    end

    return ShouldRun(DB:Get()) and true or false
end

-- ------------------------------------------------------------
-- Unit checks
-- ------------------------------------------------------------
local function IsEnemyPlayer(unit)
    if not unit or not UnitExists(unit) then
        return false
    end

    if not UnitIsPlayer(unit) then
        return false
    end

    if UnitIsUnit and UnitIsUnit("player", unit) then
        return false
    end

    -- UnitIsEnemy alone is not always stable enough for open-world PvP.
    if UnitIsEnemy and UnitIsEnemy("player", unit) then
        return true
    end

    if UnitCanAttack and UnitCanAttack("player", unit) then
        return true
    end

    return false
end

local function GetClassToken(unit)
    if not unit or not UnitExists(unit) then
        return nil
    end

    if UnitClassBase then
        local ok, classToken = pcall(UnitClassBase, unit)
        if ok and classToken then
            return classToken
        end
    end

    if UnitClass then
        local ok, _, classToken = pcall(UnitClass, unit)
        if ok and classToken then
            return classToken
        end
    end

    return nil
end

local function GetClassColor(classToken)
    if not classToken then
        return nil
    end

    local colors = CUSTOM_CLASS_COLORS or RAID_CLASS_COLORS
    if not colors then
        return nil
    end

    return colors[classToken]
end

-- ------------------------------------------------------------
-- Small helpers
-- ------------------------------------------------------------
local function GetHB(frame)
    if not frame then return nil end
    return frame.healthBar or (frame.HealthBarsContainer and frame.HealthBarsContainer.healthBar)
end

local function SameColor(r1, g1, b1, r2, g2, b2)
    return abs((r1 or 0) - (r2 or 0)) < 0.01
       and abs((g1 or 0) - (g2 or 0)) < 0.01
       and abs((b1 or 0) - (b2 or 0)) < 0.01
end

local function SafeSetAlphaFromBoolean(obj, boolMaybeSecret, aTrue, aFalse)
    if not obj then return end

    if obj.SetAlphaFromBoolean then
        pcall(obj.SetAlphaFromBoolean, obj, boolMaybeSecret, aTrue or 1, aFalse or 0)
    else
        obj:SetAlpha(aFalse or 0)
    end
end

local function SetFontSize(fs, size)
    if not fs or not fs.GetFont or not fs.SetFont then return end

    local path, _, flags = fs:GetFont()
    if path then
        fs:SetFont(path, size, flags)
    end
end

-- ------------------------------------------------------------
-- PvP class color apply/restore
-- ------------------------------------------------------------
local function RestoreIfStillPvPClassColor(hb)
    if not hb or not hb.pomPvpClassColorActive then
        return
    end

    if hb.GetStatusBarColor and hb.SetStatusBarColor and hb.pomPvpClassR then
        local r, g, b = hb:GetStatusBarColor()

        -- Only restore if the current color is still the PvP class color.
        -- If POM/PvE already repainted the bar this frame, do not overwrite it
        -- with stale old data.
        if SameColor(r, g, b, hb.pomPvpClassR, hb.pomPvpClassG, hb.pomPvpClassB) then
            hb:SetStatusBarColor(
                hb.pomPvpRestoreR or r or 1,
                hb.pomPvpRestoreG or g or 1,
                hb.pomPvpRestoreB or b or 1
            )
        end
    end

    hb.pomPvpClassColorActive = false
    hb.pomPvpClassToken = nil
    hb.pomPvpClassR = nil
    hb.pomPvpClassG = nil
    hb.pomPvpClassB = nil
end

local function ApplyClassColor(hb, unit)
    if not (hb and hb.SetStatusBarColor) then
        return false
    end

    local classToken = GetClassToken(unit)
    if not classToken then
        return false
    end

    local c = GetClassColor(classToken)
    if not c then
        return false
    end

    local cr = c.r or 1
    local cg = c.g or 1
    local cb = c.b or 1

    if hb.GetStatusBarColor then
        local r, g, b = hb:GetStatusBarColor()

        local alreadyPvPClass =
            hb.pomPvpClassColorActive
            and hb.pomPvpClassR
            and SameColor(r, g, b, hb.pomPvpClassR, hb.pomPvpClassG, hb.pomPvpClassB)

        if not alreadyPvPClass then
            hb.pomPvpRestoreR = r
            hb.pomPvpRestoreG = g
            hb.pomPvpRestoreB = b
        end
    end

    hb:SetStatusBarColor(cr, cg, cb)

    hb.pomPvpClassColorActive = true
    hb.pomPvpClassToken = classToken
    hb.pomPvpClassR = cr
    hb.pomPvpClassG = cg
    hb.pomPvpClassB = cb

    return true
end

local function QueueClassColorRetry(frame)
    if not C_Timer or not frame or frame.pomPvpClassRetryQueued then
        return
    end

    local count = frame.pomPvpClassRetryCount or 0
    if count >= 3 then
        return
    end

    frame.pomPvpClassRetryCount = count + 1
    frame.pomPvpClassRetryQueued = true

    local unit = frame.unit

    C_Timer.After(0.05, function()
        if not frame then return end

        frame.pomPvpClassRetryQueued = false

        if frame:IsForbidden() then return end
        if frame.unit ~= unit then return end
        if not UnitExists(unit) then return end

        if P and P.Apply then
            P:Apply(frame)
        end
    end)
end

-- ------------------------------------------------------------
-- Target CC container (12.1)
--
-- The old CC icon scanned 40 aura indices and picked the first one with an
-- icon. That read auras, so it died the moment auras went secret -- which in
-- a PvP match is always. An AuraContainer inverts it: we declare a filter and
-- the engine fills the button, so nothing on our side ever touches aura data.
-- "CROWD_CONTROL" is Blizzard's own classification, so the icon is also more
-- correct than "first harmful aura" ever was.
-- ------------------------------------------------------------
local CC_ICON_SIZE = 18
local CC_KEY = "cc"
local CC_FILTER = "HARMFUL|CROWD_CONTROL"

local hasAuraContainers = nil
local function HasAuraContainers()
    if hasAuraContainers == nil then
        local toc = select(4, GetBuildInfo())

        if type(toc) ~= "number" or toc < 120100 then
            hasAuraContainers = false
        else
            local ok, probe = pcall(CreateFrame, "AuraContainer", nil, UIParent,
                "CustomAuraContainerTemplate")

            hasAuraContainers = (ok and probe and type(probe.AddAuraGroup) == "function")
                and true or false

            if ok and probe then probe:Hide() end
        end
    end

    return hasAuraContainers
end

-- Blizzard invokes initializeFrame through securecallfunction, so regions
-- created here are not access-constrained. Build fresh children of the button
-- and nothing else -- reparenting an existing widget onto one is rejected,
-- and the button goes forbidden as soon as auras are secret, so this is the
-- only place the icon can be set up at all.
local function InitCCButton(btn)
    btn:SetSize(CC_ICON_SIZE, CC_ICON_SIZE)

    local ic = btn:CreateTexture(nil, "OVERLAY", nil, 7)
    ic:SetAllPoints(btn)
    ic:SetTexCoord(0.07, 0.93, 0.07, 0.93)

    -- The button feeds the icon; we never learn which aura it came from.
    btn:SetIcon(ic)
end

-- Build order is load-bearing: SetUnit, then declare the slot, then
-- SetEnabled LAST. SetEnabled is what registers the container for aura
-- events, so enabling before the slot exists leaves it registered for
-- nothing -- it renders once and then goes permanently stale.
--
-- A slot rather than a group where the client offers one: AddAuraGroup
-- creates a whole batch of buttons before maxFrameCount is applied, so a
-- one-icon group costs a batch per nameplate. AddAuraSlot creates one.
local function BuildCCContainer(holder, hb, unit)
    local ok, c = pcall(CreateFrame, "AuraContainer", nil, holder,
        "CustomAuraContainerTemplate")

    if not ok or not c then return nil end

    -- Everything below is 12.1 API this file cannot verify at load time, and
    -- a raise in here propagates out through P:Apply and kills the rest of
    -- the plate's styling. Contain the whole build: a CC icon is worth
    -- exactly one feature, never the nameplate.
    local built = pcall(function()
        c:SetPoint("LEFT", hb, "RIGHT", 6, 0)
        c:SetSize(CC_ICON_SIZE, CC_ICON_SIZE)
        c.pomUnit = unit
        c:SetUnit(unit)

        if type(c.AddAuraSlot) == "function" then
            c:AddAuraSlot(CC_KEY, CC_FILTER, { initializeFrame = InitCCButton })
        else
            c:AddAuraGroup(CC_KEY, CC_FILTER, { initializeFrame = InitCCButton })
            c:SetAuraGroupMaxFrameCount(CC_KEY, 1)
        end

        c:SetEnabled(true)
    end)

    if not built then
        pcall(c.SetEnabled, c, false)
        pcall(c.Hide, c)

        -- One failure is the answer for the session: stop building these on
        -- every plate and let the legacy path own the icon.
        hasAuraContainers = false
        return nil
    end

    return c
end

-- ------------------------------------------------------------
-- Widgets
-- ------------------------------------------------------------
local function EnsureWidgets(frame, hb)
    if not hb or hb:IsForbidden() then return end

    -- Grey HP background
    if not hb.pomPvpHB_BG then
        local bg = hb:CreateTexture(nil, "BACKGROUND", nil, 0)
        bg:SetAllPoints(hb)
        bg:SetTexture("Interface\\Buttons\\WHITE8x8")
        bg:SetVertexColor(0.18, 0.18, 0.18, 0.92)
        bg:Hide()
        hb.pomPvpHB_BG = bg
    end

    -- Target CC holder. The holder is ours, so its alpha stays writable even
    -- when the button inside it is forbidden -- that is what keeps the
    -- target-only rule working under restrictions.
    if not hb.pomPvpCCHolder then
        local h = CreateFrame("Frame", nil, hb)
        h:SetAllPoints(hb)
        h:SetAlpha(0)
        hb.pomPvpCCHolder = h
    end

    -- Standing a container up in combat lockdown is a hard client error, and
    -- nameplates are very much created in combat. Retried on every Apply, so
    -- a plate first seen mid-fight picks its container up on the next
    -- out-of-combat pass; the pool is shared, so this settles after one fight.
    if HasAuraContainers() and not hb.pomPvpCCContainer and not InCombatLockdown() then
        hb.pomPvpCCContainer = BuildCCContainer(hb.pomPvpCCHolder, hb, frame.unit)
    end

    -- Built lazily rather than up front, because HasAuraContainers() can flip
    -- to false mid-session when a build fails -- a plate that started out
    -- expecting a container still needs the scan path's texture.
    if not hb.pomPvpCCContainer and not hb.pomPvpCC and not HasAuraContainers() then
        local ic = hb.pomPvpCCHolder:CreateTexture(nil, "OVERLAY", nil, 7)
        ic:SetSize(CC_ICON_SIZE, CC_ICON_SIZE)
        ic:SetPoint("LEFT", hb, "RIGHT", 6, 0)
        ic:Hide()
        hb.pomPvpCC = ic
    end

    -- Arena number holders
    if not hb.pomArenaHolders then
        hb.pomArenaHolders = {}

        for i = 1, 5 do
            local holder = CreateFrame("Frame", nil, hb)
            holder:SetAllPoints(hb)
            holder:SetAlpha(0)

            local fs = holder:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
            fs:SetJustifyH("CENTER")
            fs:SetText(tostring(i))
            fs:SetTextColor(1, 0, 0, 1)

            holder._fs = fs
            hb.pomArenaHolders[i] = holder
        end
    end

    -- Target/focus borders
    if U and U.MakeBorder then
        if not hb.pomPvpTargetBorder then
            hb.pomPvpTargetBorder = U.MakeBorder(hb, "pomPvpTargetBorder")

            local h = CreateFrame("Frame", nil, hb)
            h:SetAllPoints(hb)
            h:SetAlpha(0)
            hb.pomPvpTargetBorderHolder = h

            for i = 1, 4 do
                local t = hb.pomPvpTargetBorder[i]
                if t then
                    t:SetParent(h)
                    t:Show()
                end
            end
        end

        if not hb.pomPvpFocusBorder then
            hb.pomPvpFocusBorder = U.MakeBorder(hb, "pomPvpFocusBorder")

            local h = CreateFrame("Frame", nil, hb)
            h:SetAllPoints(hb)
            h:SetAlpha(0)
            hb.pomPvpFocusBorderHolder = h

            for i = 1, 4 do
                local t = hb.pomPvpFocusBorder[i]
                if t then
                    t:SetParent(h)
                    t:Show()
                end
            end
        end
    end
end

local function ApplyArenaNumberLayout(hb, dbpvp)
    if not hb or not hb.pomArenaHolders then return end

    local size = tonumber(dbpvp.arenaNumbersSize or 18) or 18
    local ax   = tonumber(dbpvp.arenaNumbersX or 0) or 0
    local ay   = tonumber(dbpvp.arenaNumbersY or 8) or 8

    for i = 1, 5 do
        local h = hb.pomArenaHolders[i]
        if h and h._fs then
            h._fs:ClearAllPoints()
            h._fs:SetPoint("BOTTOM", hb, "TOP", ax, ay)
            SetFontSize(h._fs, size)
        end
    end
end

-- ------------------------------------------------------------
-- Arena numbers
-- ------------------------------------------------------------
local function UpdateArenaNumbers(frame, hb, dbpvp, inst, isEnemyPl)
    local holders = hb.pomArenaHolders
    if not holders then return end

    local enabled = (dbpvp.arenaNumbersEnabled and inst == "arena" and isEnemyPl) and true or false

    if not enabled then
        for i = 1, 5 do
            local h = holders[i]
            if h then h:SetAlpha(0) end
        end
        return
    end

    for i = 1, 5 do
        local h = holders[i]
        if h then
            if dbpvp.arenaNumbersOnly123 and i > 3 then
                h:SetAlpha(0)
            else
                SafeSetAlphaFromBoolean(h, UnitIsUnit("arena" .. i, frame.unit), 1, 0)
            end
        end
    end
end

-- ------------------------------------------------------------
-- Target CC icon
-- ------------------------------------------------------------
local function HideCC(hb)
    if hb.pomPvpCCHolder then hb.pomPvpCCHolder:SetAlpha(0) end
    if hb.pomPvpCC then hb.pomPvpCC:Hide() end
end

local function UpdateTargetCC(frame, hb, dbpvp)
    if not (dbpvp and dbpvp.showTargetCC) then
        HideCC(hb)
        return
    end

    -- 12.1: the engine owns the aura and the button, so the holder's alpha is
    -- the whole of our contribution. The container stays enabled for the life
    -- of the plate -- re-enabling it per update is what makes one go stale --
    -- and hiding is done by the holder, which is ours and stays writable.
    local c = hb.pomPvpCCContainer
    if c then
        -- Compared against our own cached token, never the container's
        -- getter: what comes back out of the engine can be secret, and
        -- comparing a secret unit token is itself restricted.
        if c.pomUnit ~= frame.unit then
            c.pomUnit = frame.unit
            c:SetUnit(frame.unit)
        end

        SafeSetAlphaFromBoolean(hb.pomPvpCCHolder, UnitIsUnit("target", frame.unit), 1, 0)
        return
    end

    -- Pre-12.1 fallback. U.AurasAreSecret() covers the restriction windows
    -- where the scan below would raise and take the rest of the
    -- CompactUnitFrame_UpdateAll chain with it -- including UpdateBorders,
    -- which runs right after us. The pcall is a backstop for restriction
    -- edges the global flag does not report.
    if not (CUA and CUA.GetAuraDataByIndex) or U.AurasAreSecret() then
        HideCC(hb)
        return
    end

    local icon = nil

    for i = 1, 40 do
        local ok, a = pcall(CUA.GetAuraDataByIndex, frame.unit, i, "HARMFUL")
        if (not ok) or (not a) then break end

        if a.icon then
            icon = a.icon
            break
        end
    end

    if icon and hb.pomPvpCC then
        hb.pomPvpCC:SetTexture(icon)
        hb.pomPvpCC:Show()
        SafeSetAlphaFromBoolean(hb.pomPvpCCHolder, UnitIsUnit("target", frame.unit), 1, 0)
    else
        HideCC(hb)
    end
end

-- ------------------------------------------------------------
-- Target/focus borders
-- ------------------------------------------------------------
local function UpdateBorders(frame, hb, dbpvp)
    if not (U and U.LayoutBorder) then
        return
    end

    if hb.pomPvpTargetBorder and hb.pomPvpTargetBorderHolder then
        if dbpvp.showTargetBorder then
            local c = dbpvp.targetBorderColor or { r = 1.0, g = 0.82, b = 0.0, a = 1.0 }

            for i = 1, 4 do
                local t = hb.pomPvpTargetBorder[i]
                if t then
                    t:SetColorTexture(c.r or 1, c.g or 0.82, c.b or 0, c.a or 1)
                end
            end

            U.LayoutBorder(hb.pomPvpTargetBorder, hb, 3)
            SafeSetAlphaFromBoolean(hb.pomPvpTargetBorderHolder, UnitIsUnit("target", frame.unit), 1, 0)
        else
            hb.pomPvpTargetBorderHolder:SetAlpha(0)
        end
    end

    if hb.pomPvpFocusBorder and hb.pomPvpFocusBorderHolder then
        if dbpvp.showFocusBorder then
            local c = dbpvp.focusBorderColor or { r = 0.0, g = 0.8, b = 1.0, a = 1.0 }

            for i = 1, 4 do
                local t = hb.pomPvpFocusBorder[i]
                if t then
                    t:SetColorTexture(c.r or 0, c.g or 0.8, c.b or 1, c.a or 1)
                end
            end

            U.LayoutBorder(hb.pomPvpFocusBorder, hb, 3)
            SafeSetAlphaFromBoolean(hb.pomPvpFocusBorderHolder, UnitIsUnit("focus", frame.unit), 1, 0)
        else
            hb.pomPvpFocusBorderHolder:SetAlpha(0)
        end
    end
end

-- ------------------------------------------------------------
-- Cleanup
-- ------------------------------------------------------------
local function Cleanup(frame)
    local hb = frame and GetHB(frame)
    if not hb or hb:IsForbidden() then
        return
    end

    if hb.pomPvpHB_BG then
        hb.pomPvpHB_BG:Hide()
    end

    if hb.pomPvpCCHolder then
        hb.pomPvpCCHolder:SetAlpha(0)
    end

    if hb.pomPvpCC then
        hb.pomPvpCC:Hide()
    end

    if hb.pomArenaHolders then
        for i = 1, 5 do
            local h = hb.pomArenaHolders[i]
            if h then h:SetAlpha(0) end
        end
    end

    if hb.pomPvpHealerSquares then
        for i = 1, 5 do
            local h = hb.pomPvpHealerSquares[i]
            if h then h:SetAlpha(0) end
        end
    end

    if hb.pomPvpTargetBorderHolder then
        hb.pomPvpTargetBorderHolder:SetAlpha(0)
    end

    if hb.pomPvpFocusBorderHolder then
        hb.pomPvpFocusBorderHolder:SetAlpha(0)
    end

    RestoreIfStillPvPClassColor(hb)

    frame.pomPvpClassRetryQueued = false
    frame.pomPvpClassRetryCount = 0
end

-- ------------------------------------------------------------
-- Apply
-- ------------------------------------------------------------
function P:Apply(frame)
    if not frame or frame:IsForbidden() then return end
    if not (U and U.IsNameplateUnit and U.IsNameplateUnit(frame.unit)) then return end
    if not (DB and DB.Get) then return end

    local dbpvp = DB:Get()

    if not ShouldRun(dbpvp) then
        Cleanup(frame)
        return
    end

    local hb = GetHB(frame)
    if not hb or hb:IsForbidden() then return end

    EnsureWidgets(frame, hb)

    local inst = GetInstType()
    local isEnemyPl = IsEnemyPlayer(frame.unit)

    -- Grey background: now also works for open-world PvP when World PvP is enabled.
    if hb.pomPvpHB_BG then
        if dbpvp.greyHPBackground and isEnemyPl and (inst == "arena" or inst == "bg" or inst == "world") then
            hb.pomPvpHB_BG:Show()
        else
            hb.pomPvpHB_BG:Hide()
        end
    end

    -- Class colors.
    -- Important:
    -- Do NOT restore back to PvE color just because one open-world check glitched.
    -- POM base color has already run before this in normal Apply(), so class color
    -- should be the final override when possible.
    if dbpvp.classColorBars and isEnemyPl and (inst == "arena" or inst == "bg" or inst == "world") then
        local applied = ApplyClassColor(hb, frame.unit)

        if applied then
            frame.pomPvpClassRetryQueued = false
            frame.pomPvpClassRetryCount = 0
        else
            QueueClassColorRetry(frame)
        end
    else
        RestoreIfStillPvPClassColor(hb)
        frame.pomPvpClassRetryQueued = false
        frame.pomPvpClassRetryCount = 0
    end

    ApplyArenaNumberLayout(hb, dbpvp)
    UpdateArenaNumbers(frame, hb, dbpvp, inst, isEnemyPl)

    if ns.pvp_healer_mark and ns.pvp_healer_mark.Apply then
        ns.pvp_healer_mark:Apply(frame, hb, inst, isEnemyPl)
    end

    UpdateTargetCC(frame, hb, dbpvp)
    UpdateBorders(frame, hb, dbpvp)
end