local _, private = ...

-- Lua Globals --
-- luacheck: globals next max floor ceil type

-- RealUI --
local RealUI = private.RealUI
local db, pointDB, barDB

local CombatFader = RealUI:GetModule("CombatFader")
local FramePoint = RealUI:GetModule("FramePoint")

local MODNAME = "ClassResource"
local ClassResource = RealUI:NewModule(MODNAME, "AceEvent-3.0", "AceBucket-3.0")

local playerClass = RealUI.charInfo.class.token
local power
local powerTextures = {
    circle = [[Interface\Addons\RealUI\Media\PointTracking\Point]],
    SOUL_SHARDS = [[Interface\Addons\RealUI\Media\PointTracking\SoulShard]],
    HOLY_POWER = {
        [[Interface\Addons\RealUI\Media\PointTracking\HolyPower1]],
        [[Interface\Addons\RealUI\Media\PointTracking\HolyPower2]],
        [[Interface\Addons\RealUI\Media\PointTracking\HolyPower3]],
        [[Interface\Addons\RealUI\Media\PointTracking\HolyPower4]],
        [[Interface\Addons\RealUI\Media\PointTracking\HolyPower5]]
    }
}

function ClassResource:GetResources()
    return self.points.info, self.bar and self.bar.info
end

function ClassResource:ForceUpdate()
    local pts, bar = ClassResource.points, ClassResource.bar
    if pts and pts.ForceUpdate then
        pts:ForceUpdate()
    end
    if bar and bar.ForceUpdate then
        bar:ForceUpdate()
    end
    CombatFader:RefreshMod()
end

-- Show or hide the DH SoulFragments bar based on the current spec.
-- Only Devourer (spec 3) has a soul fragment resource tracked by oUF's
-- ClassPower element. Havoc (spec 1) and Vengeance (spec 2) have no
-- ClassPower-compatible resource, so the bar is hidden for those specs.
--
-- oUF's ClassPower Visibility function checks GetAuraInfo() which only
-- returns POWER_TYPE_SOUL_FRAGMENTS for SPEC_DEMONHUNTER_DEVOURER (3).
-- For other specs, oUF disables the element and hides the bar. We call
-- ForceUpdate to let oUF re-run its Visibility path after the spec change,
-- which naturally shows/hides the bar based on the new spec.
--
-- CombatFader also manages the bar's visibility (alpha fading on combat
-- state changes). We use the .realUIHidden flag to prevent CombatFader
-- from showing the bar when it shouldn't be visible.
function ClassResource:SwitchDHSpec(specIndex)
    self:debug("SwitchDHSpec", specIndex)

    local pts = self.points
    if not pts then return end

    local isDevourer = (specIndex == (_G.SPEC_DEMONHUNTER_DEVOURER or 3))

    -- Set the CombatFader suppression flag. When realUIHidden is true,
    -- CombatFader:FadeIt skips the frame entirely, preventing it from
    -- being shown via UIFrameFadeIn on combat state changes.
    local barFrame = pts.frame or pts[1]
    if barFrame then
        barFrame.realUIHidden = not isDevourer
        if not isDevourer then
            barFrame:Hide()
        end
    end

    -- Trigger oUF's Visibility path which will enable/disable ClassPower
    -- based on the current spec. ForceUpdate routes through VisibilityPath
    -- which calls GetAuraInfo() → only returns a power type for Devourer.
    if pts.ForceUpdate then
        pts:ForceUpdate()
    elseif barFrame and isDevourer then
        barFrame:Show()
    end
end

local function PositionRune(rune, index)
    ClassResource:debug("PositionRune", rune, index)
    local size = pointDB.size
    local gap, middle, mod = size.gap + 2, (power.max / 2) + 0.5
    if index < middle then
        mod = index - _G.floor(middle)
    else
        mod = index - _G.ceil(middle)
    end
    rune:SetPoint("CENTER", (size.width + gap) * mod, 0)
end
local function PositionIcon(icon, index, prevIcon)
    local point, size = pointDB.reverse and "RIGHT" or "LEFT", pointDB.size
    local gap = pointDB.reverse and -(size.gap) or size.gap
    if index == 1 then
        icon:SetPoint(point)
    else
        icon:SetPoint(point, prevIcon, pointDB.reverse and "LEFT" or "RIGHT", gap, 0)
    end
end


function ClassResource:SettingsUpdate(kind, event)
    self:debug("SettingsUpdate", kind, event)
    local settings = db[kind]
    if kind == "points" then
        local frame = self.points
        for i = 1, #frame do
            local point = frame[i]
            if self.isRunes then
                point:SetSize(settings.size.width, settings.size.height)
                PositionRune(point, i)
            else
                if event == "gap" then
                    point:ClearAllPoints()
                    PositionIcon(point, i, frame[i-1])
                elseif event == "size" then
                    point:SetSize(settings.size.width, settings.size.height)
                end
            end
        end
    elseif kind == "bar" then
        if event == "size" then
            self.bar:SetSize(settings.size.width, db.size.height)
        end
    end
end

function ClassResource:CreateClassPower(unitFrame, unit)
    self:debug("CreateClassPower", unit)
    local ClassPower = {}
    local holder = _G.CreateFrame("Frame", nil, unitFrame)
    holder:SetSize(16, 16)
    ClassPower.frame = holder

    local texture, size = powerTextures[power.token], pointDB.size
    for index = 1, power.max do
        local name, icon = "ClassPower"..index
        if playerClass == "WARLOCK" then
            icon = unitFrame:CreateAngle("StatusBar", name, holder)
            icon:SetSize(size.width, size.height)
            icon:SetAngleVertex(2, 3)
            icon:SetMinMaxValues(0, 1)
        else
            icon = _G.CreateFrame("StatusBar", name, holder)
            icon:SetSize(size.width, size.height)

            local bg = icon:CreateTexture(nil, "BACKGROUND")
            bg:SetAllPoints()
            icon.bg = bg
        end

        local tex
        if playerClass == "PALADIN" then
            icon:SetPoint("CENTER")
            tex = texture[index]
        else
            PositionIcon(icon, index, ClassPower[index - 1])
            tex = texture
        end

        if not tex then
            tex = powerTextures.circle
        end
        icon:SetStatusBarTexture(tex)
        icon.bg:SetTexture(tex)
        icon.bg.multiplier = 0.5

        ClassPower[index] = icon
    end

    CombatFader:RegisterFrameForFade(MODNAME, holder)
    if self.isRunes then
        holder:SetPoint("CENTER", -160, -40.5)
    else
        FramePoint:PositionFrame(self, holder, {"class", "points", "position"})
    end

    local lastChargedIndex
    -- oUF 14: hasCurChanged inserted as arg 3, and charged points arrive as
    -- an unpacked varargs tail (..., powerType, idx1, idx2, ...) instead of
    -- a single chargedIndex. RealUI renders the first charged index only
    -- (matching its previous single-index visual capability).
    function ClassPower.PostUpdate(element, curPoints, maxPoints, hasCurChanged, hasMaxChanged, powerType, ...)
        if not curPoints or not powerType then return end
        local chargedIndex = ...
        self:debug("ClassPower:PostUpdate", curPoints, maxPoints, hasCurChanged, hasMaxChanged, powerType, chargedIndex)

        local showUnused = not pointDB.hideempty or self.configMode
        local hasPartial = (curPoints - floor(curPoints)) > 0
        local hasChargedPoint = chargedIndex and chargedIndex <= curPoints
        self:debug("data", showUnused, hasPartial, hasChargedPoint)

        local color = element.__owner.colors.power[powerType]
        local r, g, b
        if color and color.GetRGB then
            r, g, b = color:GetRGB()
        end
        local mu = element[1].bg.multiplier

        if lastChargedIndex and lastChargedIndex ~= chargedIndex and r then
            element[lastChargedIndex].bg:SetVertexColor(r * mu, g * mu, b * mu)
            element[lastChargedIndex]:SetStatusBarColor(r, g, b)
        end
        lastChargedIndex = chargedIndex

        for i = 1, maxPoints or 0 do
            local icon = element[i]
            local isPartial = hasPartial and ceil(curPoints) == i
            local isUnused = i > ceil(curPoints)
            self:debug("Icon", i, isPartial, isUnused)

            if i == chargedIndex then
                if hasChargedPoint then
                    r, g, b = 0.25, 0.75, 1
                    icon.bg:SetVertexColor(r * mu, g * mu, b * mu)
                    icon:SetStatusBarColor(r, g, b)
                end
            end

            if hasPartial and isPartial then
                icon:SetAlpha(0.5)
            else
                icon:SetAlpha(1)
            end

            if isUnused then
                if showUnused or (chargedIndex and i <= chargedIndex) then
                    icon:Show()
                else
                    icon:Hide()
                end
            else
                icon:Show()
            end
        end

        if curPoints > 0 then
            for i = 1, ceil(curPoints) do
                if element[i] then
                    element[i]:Show()
                end
            end
        end
    end

    unitFrame.ClassPower = ClassPower
    return ClassPower
end
function ClassResource:CreateRunes(unitFrame, unit)
    self:debug("CreateRunes", unit)
    local Runes = _G.CreateFrame("Frame", nil, unitFrame)
    Runes:SetSize(16, 16)
    Runes.colorSpec = true

    local size = pointDB.size
    for index = 1, power.max do
        local Rune = _G.CreateFrame("StatusBar", nil, Runes)
        Rune:SetOrientation("VERTICAL")
        Rune:SetSize(size.width, size.height)
        PositionRune(Rune, index)

        local tex = Rune:CreateTexture(nil, "ARTWORK")
        tex:SetTexture(RealUI.textures.plain)
        Rune:SetStatusBarTexture(tex)
        Rune.tex = tex

        local runeBG = Rune:CreateTexture(nil, "BACKGROUND")
        runeBG:SetTexture(RealUI.textures.plain)
        runeBG:SetPoint("TOPLEFT", Rune, -1, 1)
        runeBG:SetPoint("BOTTOMRIGHT", Rune, 1, -1)
        runeBG.multiplier = 0.2
        Rune.bg = runeBG

        Runes[index] = Rune
    end

    CombatFader:RegisterFrameForFade(MODNAME, Runes)
    FramePoint:PositionFrame(self, Runes, {"class", "points", "position"})

    function Runes.PostUpdate(element, runemap)
        local rune, runeReady, _
        for index, runeID in next, runemap do
            rune = element[index]
            _, _, runeReady = _G.GetRuneCooldown(runeID)
            if runeReady then
                rune.tex:SetAlpha(1)
            else
                rune.tex:SetAlpha(0.5)
            end
        end
    end

    unitFrame.Runes = Runes
    return Runes
end
function ClassResource:CreateSoulFragments(unitFrame, unit)
    self:debug("CreateSoulFragments", unit)
    local ClassPower = {}
    local bar = unitFrame:CreateAngle("StatusBar", nil, unitFrame)
    bar:SetSize(barDB.size.width, barDB.size.height)
    bar:SetAngleVertex(1, 3)
    bar:SetMinMaxValues(0, 1)
    bar:DisableNativeFill()

    CombatFader:RegisterFrameForFade(MODNAME, bar)
    FramePoint:PositionFrame(self, bar, {"class", "bar", "position"})

    ClassPower[1] = bar
    ClassPower.frame = bar

    -- oUF 14 signature (hasCurChanged inserted as arg 3)
    function ClassPower.PostUpdate(element, cur, maxVal, hasCurChanged, hasMaxChanged, powerType)
        self:debug("SoulFragments:PostUpdate", cur, maxVal, powerType)
        if self.configMode then
            cur = 0.6
            element[1]:SetValue(cur)
        end
        element[1]:SetShown(cur and cur > 0)
    end

    function ClassPower.PostUpdateColor(element, color)
        self:debug("SoulFragments:PostUpdateColor")
        if color and color.GetRGB then
            local r, g, b = color:GetRGB()
            element[1]:SetStatusBarColor(RealUI.ColorDarken(0.5, r, g, b))
        end
    end

    unitFrame.ClassPower = ClassPower
    return ClassPower
end
function ClassResource:CreateStagger(unitFrame, unit)
    self:debug("CreateStagger", unit)
    local Stagger = unitFrame:CreateAngle("StatusBar", nil, unitFrame)

    Stagger:SetSize(barDB.size.width, barDB.size.height)
    Stagger:SetAngleVertex(1, 3)
    Stagger:DisableNativeFill()

    FramePoint:PositionFrame(self, Stagger, {"class", "bar", "position"})

    function Stagger.PostUpdateColor(element, color)
        self:debug("Stagger:PostUpdateColor", color)
        if self.configMode then
            color = unitFrame.colors.power.STAGGER and unitFrame.colors.power.STAGGER[2]
        end
        if color then
            local r, g, b
            if color.GetRGB then
                r, g, b = color:GetRGB()
            else
                r, g, b = color[1] or color.r, color[2] or color.g, color[3] or color.b
            end
            if r then
                element:SetStatusBarColor(RealUI.ColorDarken(0.5, r, g, b))
            end
        end
    end
    function Stagger.PostUpdate(element, cur, max)
        local r, g, b = element:GetStatusBarColor()
        self:debug("Stagger:PostUpdate", cur, max, r, g, b)
        if self.configMode then
            cur = max * 0.3
            element:SetValue(cur)
        end
        element:SetShown(cur > 0)
    end

    unitFrame.Stagger = Stagger
    return Stagger
end

function ClassResource:Setup(unitFrame, unit)
    -- Store unitFrame for potential future use by SwitchDHSpec
    self._unitFrame = unitFrame

    local isEnabled = ClassResource:IsEnabled()

    -- Demon Hunter: only Devourer (spec 3) has a soul fragment resource
    -- tracked by oUF's ClassPower element. Create the SoulFragments bar
    -- unconditionally; oUF's Visibility function will show/hide it based
    -- on the active spec. For Havoc/Vengeance, the bar stays hidden.
    if playerClass == "DEMONHUNTER" then
        if isEnabled then
            self.points = self:CreateSoulFragments(unitFrame, unit)
        else
            self.points = {}
        end
        self.points.info = {token = power.token, name = _G[power.token] or power.name or power.token}

        -- Let oUF handle visibility; call SwitchDHSpec to pre-hide if not
        -- Devourer (before oUF Enable runs).
        self:SwitchDHSpec(_G.GetSpecialization() or 1)
        return
    end

    -- Points
    if isEnabled then
        if playerClass == "DEATHKNIGHT" then
            self.points = self:CreateRunes(unitFrame, unit)
        else
            self.points = self:CreateClassPower(unitFrame, unit)
        end
    else
        self.points = {}
    end
    self.points.info = {token = power.token, name = _G[power.token] or power.name or power.token}

    -- Bars
    if playerClass == "MONK" then
        self.bar = isEnabled and self:CreateStagger(unitFrame, unit) or {}
        self.bar.info = _G.C_Spell.GetSpellInfo(124255)
    end
end

function ClassResource:ToggleConfigMode(isConfigMode)
    if self.configMode == isConfigMode then return end
    self.configMode = isConfigMode

    if isConfigMode then
        local pts, bar = self.points, self.bar
        local ptsFrame = pts and pts.frame
        if ptsFrame and ptsFrame.SetAlpha then
            ptsFrame:SetAlpha(1)
            for i = 1, power.max do
                if self.isRunes then
                    if pts[i] and pts[i].SetValue then
                        pts[i]:SetValue(i / power.max)
                    end
                else
                    if pts[i] and pts[i].SetShown then
                        pts[i]:SetShown(true)
                    end
                end
            end
            if not self.isRunes and pts.PostUpdate then
                pts:PostUpdate(3, 5, true, power.type)
            end
        end
        if bar and bar.SetMinMaxValues and bar.PostUpdate then
            local maxHealth = _G.UnitHealthMax("player")
            bar:SetMinMaxValues(0, maxHealth)
            bar:PostUpdate(maxHealth * 0.4, maxHealth)
        end
        -- DH Soul Fragments bar in config mode. The bar uses normalized 0-1
        -- values and only exists for Devourer (spec 3). Show a preview fill
        -- so the user can see the bar in config mode regardless of current
        -- soul fragment count.
        if playerClass == "DEMONHUNTER" and pts
            and pts[1] and pts[1].SetMinMaxValues then
            pts[1]:SetMinMaxValues(0, 1)
            pts[1]:SetValue(0.6)
            pts[1]:Show()
        end
    else
        -- Explicitly undo the SetShown(true) calls from config mode
        -- before ForceUpdate restores the correct display state
        local pts = self.points
        if pts then
            for i = 1, power.max do
                if pts[i] and pts[i].Hide then
                    pts[i]:Hide()
                end
            end
        end
        ClassResource:ForceUpdate()
    end
end

local classPowers = {
    default = {type = _G.Enum.PowerType.ComboPoints, token = "COMBO_POINTS"},
    DEATHKNIGHT = {
        type = _G.Enum.PowerType.Runes,
        token = "RUNES",
        max = 6
    },
    -- name: explicit display label for tokens that have no _G[token] global string.
    -- Tokens like COMBO_POINTS, CHI, HOLY_POWER, etc. resolve via _G[token] at
    -- runtime so they pick up localization automatically.  SOUL_FRAGMENTS,
    -- TIP_OF_THE_SPEAR, and POWER_TYPE_ESSENCE are internal/alternate-power
    -- identifiers that Blizzard does not expose as global strings.
    HUNTER = {token = "TIP_OF_THE_SPEAR", max = 3, name = "Tip of the Spear"},
    MAGE = {type = _G.Enum.PowerType.ArcaneCharges, token = "ARCANE_CHARGES", max = 5}, -- 4 for Arcane, 5 for Frost Icicles
    MONK = {type = _G.Enum.PowerType.Chi, token = "CHI"},
    PALADIN = {
        type = _G.Enum.PowerType.HolyPower,
        token = "HOLY_POWER",
        max = 5
    },
    DEMONHUNTER = {token = "SOUL_FRAGMENTS", max = 1, name = "Soul Fragments"},
    SHAMAN = {token = "MAELSTROM", max = 10},
    WARLOCK = {type = _G.Enum.PowerType.SoulShards, token = "SOUL_SHARDS"},
    EVOKER = {type = _G.Enum.PowerType.Essence, token = "POWER_TYPE_ESSENCE", name = "Essence"},
}
function ClassResource:OnInitialize()
    self:debug("OnInitialize")
    local points do
        points = {
            hideempty = true, -- Only show used icons
            reverse = false, -- Points start on the right
            size = {
                width = 13,
                height = 13,
                gap = 2,
            },
            position = {
                --[[ B12: class points pinned to the player frame by DEFAULT
                     (2026-08-23). Arnvid's placement, left-below the player
                     frame, re-measured 2026-08-22 AFTER the ClassResource
                     drag-save fix — the earlier readings were LibWindow
                     screen coords masquerading as frame-relative offsets.
                     Matches the `/realui newdefaults` "pinned" item. ]]
                x = -85,
                y = -25,
                point = "CENTER",
                -- "screen" | "player" | "target" | "focus" (FramePoint.ANCHOR_FRAMES)
                anchorTo = "player",
            },
        }

        if playerClass == "DEATHKNIGHT" then
            points.size.width = 9
            points.size.height = 38
            points.size.gap = 1

            points.position.x = 0
            points.position.y = -110
        elseif playerClass == "PALADIN" then
            points.size.width = 64
            points.size.height = 64

            points.position.x = 0
            points.position.y = -110
        elseif playerClass == "WARLOCK" then
            points.size.width = 22
            points.size.height = 13
            points.size.gap = -5
        end
    end

    self.db = RealUI.db:RegisterNamespace(MODNAME)
    self.db:RegisterDefaults({
        class = {
            locked = true,
            bar = {
                size = {
                    width = 200,
                    height = 8,
                },
                position = {
                    x = 0,
                    y = -128,
                    point = "CENTER",
                    anchorTo = "screen",
                },
            },
            points = points,
            combatfade = {
                enabled = true,
                opacity = {
                    incombat = 1,
                    harmtarget = .8,
                    target = .8,
                    hurt = .5,
                    outofcombat = playerClass == "DEATHKNIGHT" and 0 or .3,
                }
            }
        }
    })
    db = self.db.class
    pointDB, barDB = db.points, db.bar

    power = classPowers[playerClass] or classPowers.default
    power.max = power.max or 10
    self.isRunes = power.type == _G.Enum.PowerType.Runes

    local isEnabled = RealUI:GetModuleEnabled(MODNAME)
    self:SetEnabledState(isEnabled)
end

-- Same wrapper CastBars uses: the DEFAULT LibWindow drag handlers save
-- SCREEN coordinates into the position config, but ApplyAnchor reads that
-- config as UNIT-FRAME-relative offsets whenever anchorTo is set — so every
-- drag of pinned class points made the frame teleport on the next refresh
-- ("keeps moving", found live 2026-08-22 during the B10 layout session).
-- FramePoint.OnDragStop converts drags into anchor-relative offsets.
local function OnDragStop(frame)
    FramePoint.OnDragStop(frame)
    _G.LibStub("AceConfigRegistry-3.0"):NotifyChange("HuD")
end

function ClassResource:OnEnable()
    self:debug("OnEnable")

    CombatFader:RegisterModForFade(MODNAME, "class", "combatfade")
    FramePoint:RegisterMod(self, nil, OnDragStop)

    -- Demon Hunter: listen for spec changes so the SoulFragments bar
    -- shows/hides correctly when switching to/from Devourer (spec 3).
    if playerClass == "DEMONHUNTER" then
        self:RegisterEvent("PLAYER_SPECIALIZATION_CHANGED", "OnSpecChanged")
    end
end

function ClassResource:OnSpecChanged()
    self:debug("OnSpecChanged")
    self:SwitchDHSpec(_G.GetSpecialization() or 1)
end

function ClassResource:OnDisable()
    self:debug("OnEnable")
end
