-- Modules/MSUF_PortraitDecoration.lua
-- Portrait decoration: borders, backgrounds, size override, offset, strata elevation.
-- Loads AFTER Core/MSUF_Portraits.lua in the TOC.
--
-- Architecture:
--   - portraitContainer frame: reparents portrait texture for strata elevation (above HP/power bars)
--   - ComputeAndApplyLayout: always runs (cheap), asserts size/offset/container after parent layout
--   - Decoration (border, bg, shape): stamp-gated, only updates on settings change
--
-- DB contract: all portrait* keys live on per-unit conf (MSUF_DB[unitKey].*).
--   ScopeSet (Options panel) writes to per-unit directly; no runtime fallback needed.
--
-- Secret-safe. Zero combat cost.

local addonName, ns = ...

local type, tonumber, math_max = type, tonumber, math.max
local UnitClassBase = UnitClassBase or (C_UnitInfo and C_UnitInfo.GetUnitClassBase)
local UnitReaction  = UnitReaction
local RAID_CLASS_COLORS = RAID_CLASS_COLORS

local TEX_WHITE8 = "Interface\\Buttons\\WHITE8x8"
local ADDON_PATH = "Interface\\AddOns\\" .. (addonName or "MidnightSimpleUnitFrames")
local RING_CIRCLE = ADDON_PATH .. "\\Media\\Borders\\circle_ring_mask.tga"
local SHAPE_RING  = { CIRCLE = RING_CIRCLE }
local RONDO_PACKS = { RONDO_COLOR = true, RONDO_WOW = true }

-- ────────────────────────────────────────────────────────────
-- Value reader (1 table lookup — ScopeSet writes per-unit directly)
-- ────────────────────────────────────────────────────────────
local function V(conf, key, def)
    local v = conf[key]
    return (v ~= nil) and v or def
end

local function GV(key, def)
    local g = _G.MSUF_DB and _G.MSUF_DB.general
    if type(g) == "table" and g[key] ~= nil then return g[key] end
    return def
end

-- ────────────────────────────────────────────────────────────
-- Decoration stamp (all visual fields)
-- ────────────────────────────────────────────────────────────
local function DecoStamp(conf)
    return V(conf,"portraitShape","Q") .. "|" ..
           V(conf,"portraitBorderStyle","N") .. "|" ..
           V(conf,"portraitBorderThickness",2) .. "|" ..
           GV("portraitBorderColorR",0) .. "|" ..
           GV("portraitBorderColorG",0) .. "|" ..
           GV("portraitBorderColorB",0) .. "|" ..
           GV("portraitBorderColorA",1) .. "|" ..
           (V(conf,"portraitBgEnabled",false) and "1" or "0") .. "|" ..
           GV("portraitBgColorR",0.05) .. "|" ..
           GV("portraitBgColorG",0.05) .. "|" ..
           GV("portraitBgColorB",0.05) .. "|" ..
           GV("portraitBgColorA",0.85) .. "|" ..
           V(conf,"portraitClassStyle","B") .. "|" ..
           (conf.portraitRender or "2D")
end

-- Layout stamp (only position/size relevant fields)
local function LayoutStamp(conf)
    return V(conf,"portraitSizeOverride",0) .. "|" ..
           V(conf,"portraitOffsetX",0) .. "|" ..
           V(conf,"portraitOffsetY",0) .. "|" ..
           (V(conf,"portraitFillBorder",false) and "1" or "0") .. "|" ..
           V(conf,"portraitBorderThickness",2) .. "|" ..
           V(conf,"portraitBorderStyle","N") .. "|" ..
           (conf.portraitMode or "X") .. "|" ..
           (conf.height or 0)
end

-- ────────────────────────────────────────────────────────────
-- Lazy decoration frame
-- ────────────────────────────────────────────────────────────
local function EnsureDecor(f)
    local d = f._msufPortraitDecor
    if d then return d end
    d = CreateFrame("Frame", nil, f)
    d:SetFrameStrata(f:GetFrameStrata())
    d.bg = d:CreateTexture(nil, "BACKGROUND", nil, -1)
    d.bg:SetTexture(TEX_WHITE8); d.bg:Hide()
    d.portraitContainer = CreateFrame("Frame", nil, f)
    d.portraitContainer:SetFrameStrata(f:GetFrameStrata())
    d.borderFrame = CreateFrame("Frame", nil, f)
    d.borderFrame:SetFrameStrata(f:GetFrameStrata())
    d.shapedBorder = d.borderFrame:CreateTexture(nil, "OVERLAY", nil, 3)
    d.shapedBorder:Hide()
    d.edgeT = d.borderFrame:CreateTexture(nil, "OVERLAY", nil, 3)
    d.edgeB = d.borderFrame:CreateTexture(nil, "OVERLAY", nil, 3)
    d.edgeL = d.borderFrame:CreateTexture(nil, "OVERLAY", nil, 3)
    d.edgeR = d.borderFrame:CreateTexture(nil, "OVERLAY", nil, 3)
    local edges = { d.edgeT, d.edgeB, d.edgeL, d.edgeR }
    for i = 1, 4 do edges[i]:SetTexture(TEX_WHITE8); edges[i]:Hide() end
    d._edges = edges
    f._msufPortraitDecor = d
    return d
end

-- ────────────────────────────────────────────────────────────
-- LAYOUT: position, size, container reparent, strata.
-- Runs every hook call but stamp-gated internally.
-- Forces re-apply when portrait parent changed (parent system repositioned it).
-- ────────────────────────────────────────────────────────────
local function ComputeAndApplyLayout(f, conf, portrait)
    local mode = conf.portraitMode or "OFF"
    if mode ~= "LEFT" and mode ~= "RIGHT" then return end

    local d = f._msufPortraitDecor

    -- Layout stamp: skip if nothing layout-relevant changed AND portrait is in container
    local lStamp = LayoutStamp(conf)
    local inContainer = d and d.portraitContainer and portrait.GetParent and portrait:GetParent() == d.portraitContainer
    if inContainer and f._msufLayoutStamp == lStamp then return end
    f._msufLayoutStamp = lStamp

    local anchor = f.hpBar or f
    if f._msufPowerBarReserved then anchor = f end

    local h = conf.height or (f.GetHeight and f:GetHeight()) or 30
    local autoSize = math_max(16, h - 4)
    local sizeOvr = tonumber(V(conf, "portraitSizeOverride", 0)) or 0
    local size = (sizeOvr > 0) and math_max(16, sizeOvr) or autoSize

    local ox = tonumber(V(conf, "portraitOffsetX", 0)) or 0
    local oy = tonumber(V(conf, "portraitOffsetY", 0)) or 0

    -- Strata hierarchy
    local baseLevel = f.hpBar and f.hpBar:GetFrameLevel() or (f:GetFrameLevel() + 1)
    if d then
        d:SetFrameLevel(baseLevel + 5)
        if d.portraitContainer then d.portraitContainer:SetFrameLevel(baseLevel + 6) end
        d.borderFrame:SetFrameLevel(baseLevel + 7)
    end

    -- Reparent portrait into elevated container
    if d and d.portraitContainer then
        local pc = d.portraitContainer
        if portrait.GetParent and portrait:GetParent() ~= pc then
            portrait:SetParent(pc)
        end
        pc:ClearAllPoints(); pc:SetSize(size, size)
        if mode == "LEFT" then pc:SetPoint("RIGHT", anchor, "LEFT", ox, oy)
        else pc:SetPoint("LEFT", anchor, "RIGHT", ox, oy) end
        pc:Show()
        portrait:ClearAllPoints(); portrait:SetAllPoints(pc)
        portrait:SetDrawLayer("ARTWORK", 0)
    else
        portrait:ClearAllPoints(); portrait:SetSize(size, size)
        if mode == "LEFT" then portrait:SetPoint("RIGHT", anchor, "LEFT", ox, oy)
        else portrait:SetPoint("LEFT", anchor, "RIGHT", ox, oy) end
    end

end

-- ────────────────────────────────────────────────────────────
-- Border color (secret-safe)
-- ────────────────────────────────────────────────────────────
local function ResolveBorderColor(conf, unit)
    local style = V(conf, "portraitBorderStyle", "NONE")
    if style == "NONE" then return nil end
    if style == "CUSTOM" or style == "SOLID" then
        return GV("portraitBorderColorR",1), GV("portraitBorderColorG",1),
               GV("portraitBorderColorB",1), GV("portraitBorderColorA",1)
    end
    if style == "CLASS_COLOR" then
        local class = UnitClassBase and UnitClassBase(unit)
        if class and RAID_CLASS_COLORS then
            local c = RAID_CLASS_COLORS[class]
            if c then return c.r, c.g, c.b, 1 end
        end
        return 1, 1, 1, 1
    end
    if style == "REACTION" then
        local reaction = tonumber(UnitReaction and UnitReaction(unit, "player"))
        if reaction then
            if reaction <= 2 then return 1, 0, 0, 1 end
            if reaction <= 4 then return 1, 0.6, 0, 1 end
            return 0, 1, 0, 1
        end
        return 1, 1, 1, 1
    end
    return 1, 1, 1, 1
end

-- ────────────────────────────────────────────────────────────
-- Shape TexCoord
-- ────────────────────────────────────────────────────────────
local function ApplyShapeTexCoord(portrait, shape, isRondo, preserveTexCoord)
    if not portrait or not portrait.SetTexCoord then return end
    if preserveTexCoord then return end
    if isRondo then portrait:SetTexCoord(0, 1, 0, 1); return end
    if shape == "CIRCLE" then
        portrait:SetTexCoord(0.08, 0.92, 0.08, 0.92)
    else
        portrait:SetTexCoord(0.1, 0.9, 0.1, 0.9)
    end
end

-- ────────────────────────────────────────────────────────────
-- Background
-- ────────────────────────────────────────────────────────────
local function ApplyBackground(d, conf, portrait)
    if not V(conf, "portraitBgEnabled", false) then d.bg:Hide(); return end
    d.bg:ClearAllPoints()
    d.bg:SetPoint("TOPLEFT", portrait, "TOPLEFT", -1, 1)
    d.bg:SetPoint("BOTTOMRIGHT", portrait, "BOTTOMRIGHT", 1, -1)
    d.bg:SetVertexColor(GV("portraitBgColorR",0.05), GV("portraitBgColorG",0.05),
                        GV("portraitBgColorB",0.05), GV("portraitBgColorA",0.85))
    local pLevel = (portrait.GetParent and portrait:GetParent() and portrait:GetParent().GetFrameLevel)
        and portrait:GetParent():GetFrameLevel() or 0
    d:SetFrameLevel(math_max(0, pLevel)); d.bg:SetDrawLayer("BACKGROUND", -1); d.bg:Show()
end

-- ────────────────────────────────────────────────────────────
-- Border
-- ────────────────────────────────────────────────────────────
local function ApplyBorder(d, conf, portrait, shape, r, g, b, a)
    if not r then
        d.shapedBorder:Hide()
        for i = 1, 4 do d._edges[i]:Hide() end
        return
    end
    local thick = math_max(1, tonumber(V(conf, "portraitBorderThickness", 2)) or 2)
    local drawInside = V(conf, "portraitFillBorder", false) == true
    local ringTex = SHAPE_RING[shape]
    if ringTex then
        for i = 1, 4 do d._edges[i]:Hide() end
        d.shapedBorder:SetTexture(ringTex); d.shapedBorder:SetTexCoord(0, 1, 0, 1)
        d.shapedBorder:ClearAllPoints()
        if drawInside then
            d.shapedBorder:SetAllPoints(portrait)
        else
            d.shapedBorder:SetPoint("TOPLEFT", portrait, "TOPLEFT", -thick, thick)
            d.shapedBorder:SetPoint("BOTTOMRIGHT", portrait, "BOTTOMRIGHT", thick, -thick)
        end
        d.shapedBorder:SetVertexColor(r, g, b, a); d.shapedBorder:Show()
    else
        d.shapedBorder:Hide()
        local eT, eB, eL, eR = d.edgeT, d.edgeB, d.edgeL, d.edgeR
        if drawInside then
            eT:ClearAllPoints(); eT:SetPoint("TOPLEFT", portrait, "TOPLEFT", 0, 0)
            eT:SetPoint("TOPRIGHT", portrait, "TOPRIGHT", 0, 0)
            eT:SetHeight(thick); eT:SetVertexColor(r,g,b,a); eT:Show()
            eB:ClearAllPoints(); eB:SetPoint("BOTTOMLEFT", portrait, "BOTTOMLEFT", 0, 0)
            eB:SetPoint("BOTTOMRIGHT", portrait, "BOTTOMRIGHT", 0, 0)
            eB:SetHeight(thick); eB:SetVertexColor(r,g,b,a); eB:Show()
            eL:ClearAllPoints(); eL:SetPoint("TOPLEFT", portrait, "TOPLEFT", 0, 0)
            eL:SetPoint("BOTTOMLEFT", portrait, "BOTTOMLEFT", 0, 0)
            eL:SetWidth(thick); eL:SetVertexColor(r,g,b,a); eL:Show()
            eR:ClearAllPoints(); eR:SetPoint("TOPRIGHT", portrait, "TOPRIGHT", 0, 0)
            eR:SetPoint("BOTTOMRIGHT", portrait, "BOTTOMRIGHT", 0, 0)
            eR:SetWidth(thick); eR:SetVertexColor(r,g,b,a); eR:Show()
        else
            eT:ClearAllPoints(); eT:SetPoint("TOPLEFT", portrait, "TOPLEFT", -thick, thick)
            eT:SetPoint("TOPRIGHT", portrait, "TOPRIGHT", thick, thick)
            eT:SetHeight(thick); eT:SetVertexColor(r,g,b,a); eT:Show()
            eB:ClearAllPoints(); eB:SetPoint("BOTTOMLEFT", portrait, "BOTTOMLEFT", -thick, -thick)
            eB:SetPoint("BOTTOMRIGHT", portrait, "BOTTOMRIGHT", thick, -thick)
            eB:SetHeight(thick); eB:SetVertexColor(r,g,b,a); eB:Show()
            eL:ClearAllPoints(); eL:SetPoint("TOPLEFT", portrait, "TOPLEFT", -thick, thick)
            eL:SetPoint("BOTTOMLEFT", portrait, "BOTTOMLEFT", -thick, -thick)
            eL:SetWidth(thick); eL:SetVertexColor(r,g,b,a); eL:Show()
            eR:ClearAllPoints(); eR:SetPoint("TOPRIGHT", portrait, "TOPRIGHT", thick, thick)
            eR:SetPoint("BOTTOMRIGHT", portrait, "BOTTOMRIGHT", thick, -thick)
            eR:SetWidth(thick); eR:SetVertexColor(r,g,b,a); eR:Show()
        end
    end
    local pLevel = (portrait.GetParent and portrait:GetParent() and portrait:GetParent().GetFrameLevel)
        and portrait:GetParent():GetFrameLevel() or 0
    d.borderFrame:SetFrameLevel(pLevel + 3)
end

-- ────────────────────────────────────────────────────────────
-- Hide all decoration
-- ────────────────────────────────────────────────────────────
local function HideAllDecor(f)
    local d = f._msufPortraitDecor
    if not d then return end
    d:Hide(); d.bg:Hide(); d.shapedBorder:Hide(); d.borderFrame:Hide()
    if d.portraitContainer then d.portraitContainer:Hide() end
    for i = 1, 4 do d._edges[i]:Hide() end
end

-- ────────────────────────────────────────────────────────────
-- MAIN ENTRY
-- ────────────────────────────────────────────────────────────
local function MSUF_ApplyPortraitDecoration(f, unit, conf, existsForPortrait)
    if not f or not conf then return end
    local portrait = f.portrait
    local mode = conf.portraitMode or "OFF"

    if mode == "OFF" or not portrait then HideAllDecor(f); return end
    if not existsForPortrait then HideAllDecor(f); return end

    local d = EnsureDecor(f)

    -- LAYOUT: always re-assert (parent system may have overridden)
    -- Internally stamp-gated + parent-check for efficiency
    ComputeAndApplyLayout(f, conf, portrait)

    -- DECORATION: stamp-gated (border/bg/shape)
    local stamp = DecoStamp(conf)
    local uStamp = unit or ""
    if f._msufDecoStamp == stamp and f._msufDecoUnitStamp == uStamp then
        d:Show(); d.borderFrame:Show()
        return
    end
    f._msufDecoStamp = stamp
    f._msufDecoUnitStamp = uStamp

    local shape = V(conf, "portraitShape", "SQUARE")
    local render = conf.portraitRender or "2D"
    local classStyle = V(conf, "portraitClassStyle", "BLIZZARD")
    local isClassRender = render == "CLASS"
    local isRondo = isClassRender and RONDO_PACKS[classStyle] or false
    local preserveClassAtlasCoords = isClassRender and not isRondo

    ApplyShapeTexCoord(portrait, shape, isRondo, preserveClassAtlasCoords)
    ApplyBackground(d, conf, portrait)

    if isRondo then
        d.shapedBorder:Hide()
        for i = 1, 4 do d._edges[i]:Hide() end
    else
        local br, bg_c, bb, ba = ResolveBorderColor(conf, unit)
        ApplyBorder(d, conf, portrait, shape, br, bg_c, bb, ba)
    end

    d:Show(); d.borderFrame:Show()
end

_G.MSUF_ApplyPortraitDecoration = MSUF_ApplyPortraitDecoration
_G.MSUF_IsRondoPortraitPack = function(conf)
    return RONDO_PACKS[V(conf, "portraitClassStyle", "BLIZZARD")] or false
end

-- ────────────────────────────────────────────────────────────
-- Hooks
-- ────────────────────────────────────────────────────────────
if type(hooksecurefunc) == "function" then
    -- After portrait rendering (2D/CLASS texture is set)
    if type(_G.MSUF_UpdatePortraitIfNeeded) == "function" then
        local function OnUpdatePortrait(f, unit, conf, exists)
            local fn = _G.MSUF_ApplyPortraitDecoration
            if type(fn) == "function" then fn(f, unit, conf, exists) end
        end
        hooksecurefunc("MSUF_UpdatePortraitIfNeeded", OnUpdatePortrait)
    end
    -- After parent layout (ClearAllPoints + SetSize + SetPoint on portrait)
    if type(_G.MSUF_UpdateBossPortraitLayout) == "function" then
        local function OnBossPortraitLayout(f, conf)
            if not f or not f.portrait or not conf then return end
            if (conf.portraitMode or "OFF") == "OFF" then return end
            -- Invalidate layout stamp so ComputeAndApplyLayout re-asserts
            f._msufLayoutStamp = nil
            ComputeAndApplyLayout(f, conf, f.portrait)
        end
        hooksecurefunc("MSUF_UpdateBossPortraitLayout", OnBossPortraitLayout)
    end
    -- OFF-gate cleanup
    if type(_G.MSUF_MaybeUpdatePortrait) == "function" then
        local function OnMaybeUpdatePortrait(f, unit, conf)
            if not f or not conf then return end
            if (conf.portraitMode or "OFF") == "OFF" then HideAllDecor(f) end
        end
        hooksecurefunc("MSUF_MaybeUpdatePortrait", OnMaybeUpdatePortrait)
    end
end

-- ────────────────────────────────────────────────────────────
-- Event-driven stamp invalidation
-- Ensures decoration is applied/refreshed when units appear/change.
-- ────────────────────────────────────────────────────────────
local function InvalidateFrame(f)
    if not f then return end
    f._msufDecoStamp = nil
    f._msufDecoUnitStamp = nil
    f._msufLayoutStamp = nil
end

local function InvalidateByKey(key)
    if key == "boss" then
        for i = 1, 5 do InvalidateFrame(_G["MSUF_boss" .. i]) end
    else
        InvalidateFrame(_G["MSUF_" .. key])
    end
end

local function InvalidateAll()
    for _, k in ipairs({ "player", "target", "focus", "pet", "targettarget" }) do
        InvalidateByKey(k)
    end
    InvalidateByKey("boss")
end

local function SyncUnitSafe(key)
    local fn = _G.MSUF_PortraitDecoration_SyncUnit
    if type(fn) == "function" then fn(key) end
end

local function RefreshAllTimer()
    InvalidateAll()
    local fn = _G.MSUF_PortraitDecoration_RefreshAll
    if type(fn) == "function" then fn() end
end

local function SyncTargetTimer()
    SyncUnitSafe("target")
    SyncUnitSafe("targettarget")
end

local function SyncFocusTimer()
    SyncUnitSafe("focus")
end

local function SyncPetTimer()
    SyncUnitSafe("pet")
end

local function SyncBossTimer()
    SyncUnitSafe("boss")
end

local function SchedulePortraitTimer(delay, label, fn)
    if C_Timer and C_Timer.After then
        C_Timer.After(delay, fn)
    end
end

do
    local ev = CreateFrame("Frame")
    _G.MSUF_PortraitDecorationEventFrame = ev
    ev:RegisterEvent("PLAYER_ENTERING_WORLD")
    ev:RegisterEvent("PLAYER_TARGET_CHANGED")
    ev:RegisterEvent("PLAYER_FOCUS_CHANGED")
    ev:RegisterEvent("UNIT_PET")
    ev:RegisterEvent("INSTANCE_ENCOUNTER_ENGAGE_UNIT")
    ev:SetScript("OnEvent", function(self, event)
        -- PERF: Skip ALL portrait work when portraits are OFF for all units.
        -- This saves InvalidateByKey + SyncUnitSafe + HideAllDecor per target click.
        local db = _G.MSUF_DB
        if db then
            local allOff = true
            for _, k in ipairs({"player","target","focus","pet","targettarget","boss"}) do
                local c = db[k]
                if c and (c.portraitMode or "OFF") ~= "OFF" then allOff = false; break end
            end
            if allOff then return end
        end

        if event == "PLAYER_ENTERING_WORLD" then
            SchedulePortraitTimer(0.1, "PortraitDecoration.Timer.RefreshAll.0.1", RefreshAllTimer)
            SchedulePortraitTimer(0.5, "PortraitDecoration.Timer.RefreshAll.0.5", RefreshAllTimer)
        elseif event == "PLAYER_TARGET_CHANGED" then
            InvalidateByKey("target"); InvalidateByKey("targettarget")
            SchedulePortraitTimer(0, "PortraitDecoration.Timer.SyncTarget", SyncTargetTimer)
        elseif event == "PLAYER_FOCUS_CHANGED" then
            InvalidateByKey("focus")
            SchedulePortraitTimer(0, "PortraitDecoration.Timer.SyncFocus", SyncFocusTimer)
        elseif event == "UNIT_PET" then
            InvalidateByKey("pet")
            SchedulePortraitTimer(0, "PortraitDecoration.Timer.SyncPet", SyncPetTimer)
        elseif event == "INSTANCE_ENCOUNTER_ENGAGE_UNIT" then
            InvalidateByKey("boss")
            SchedulePortraitTimer(0, "PortraitDecoration.Timer.SyncBoss", SyncBossTimer)
        end
    end)
end

-- ────────────────────────────────────────────────────────────
-- Sync helpers (Options panel live-apply)
-- ────────────────────────────────────────────────────────────
local function GetFramesForUnitKey(key)
    if key == "tot" then key = "targettarget" end
    if key == "boss" then
        local t = {}
        for i = 1, 5 do local f = _G["MSUF_boss"..i]; if f then t[#t+1] = f end end
        return t
    end
    local f = _G["MSUF_" .. (key or "")]
    if not f and key == "targettarget" then f = _G.MSUF_targettarget or _G.MSUF_tot end
    return f and { f } or {}
end

local function SyncDecorationUnit(unitKey)
    if type(unitKey) ~= "string" or unitKey == "" then return end
    local db = _G.MSUF_DB; if not db then return end
    local conf = (unitKey == "boss") and db.boss or db[unitKey]
    if unitKey == "tot" then conf = db.targettarget or db.tot end
    if not conf then return end
    local frames = GetFramesForUnitKey(unitKey)
    for i = 1, #frames do
        local f = frames[i]
        if f then
            InvalidateFrame(f)
            local unit = f.unit or unitKey
            local exists = UnitExists and UnitExists(unit) or false
            local fn = _G.MSUF_ApplyPortraitDecoration
            if type(fn) == "function" then fn(f, unit, conf, exists) end
        end
    end
end
_G.MSUF_PortraitDecoration_SyncUnit = SyncDecorationUnit

local function RefreshAllDecoration()
    for _, k in ipairs({"player","target","focus","pet","targettarget"}) do
        _G.MSUF_PortraitDecoration_SyncUnit(k)
    end
    _G.MSUF_PortraitDecoration_SyncUnit("boss")
end
_G.MSUF_PortraitDecoration_RefreshAll = RefreshAllDecoration
