-- modules/PartyTrinkets.lua
-- Friendly party trinket tracking for ArenaCore -- because knowing if your healer trinket is down matters

local _, addon = ...
local AC = _G.ArenaCore
if not AC then return end

AC.PartyTrinkets = AC.PartyTrinkets or {}
local PT = AC.PartyTrinkets

local __ptFrames       = setmetatable({}, { __mode = "k" })
local __ptEventFrame   = nil
local __ptTestFrames   = {}
local __ptTestActive   = false
local __ptTestWasSolo   = false

local TRINKET_ICON = 1322720 -- PvP Trinket icon

local function IsGroupUnit(unit)
    if not unit then return false end
    return unit == "player"
        or unit:match("^party%d$") ~= nil
        or unit:match("^raid%d+$") ~= nil
end

local function IsMatchActive()
    if not C_PvP or not C_PvP.GetActiveMatchState or not Enum or not Enum.PvPMatchState then
        return true
    end

    local state = C_PvP.GetActiveMatchState()
    return state and state >= Enum.PvPMatchState.Engaged
end

local function IsInPvPInstance()
    -- Only show in PvP content: arenas, battlegrounds, solo shuffle
    -- Hide in dungeons, raids, and other PvE content
    local inInstance, instanceType = IsInInstance()
    if not inInstance then
        return false
    end
    return instanceType == "arena" or instanceType == "pvp"
end

local function RequestTrinketSpell(unit)
    if unit and C_PvP and C_PvP.RequestCrowdControlSpell and IsMatchActive() then
        pcall(C_PvP.RequestCrowdControlSpell, unit)
    end
end

local function ForEachCompactFrame(fn)
    if CompactPartyFrame and CompactPartyFrame.memberUnitFrames then
        for _, f in ipairs(CompactPartyFrame.memberUnitFrames) do
            if f then fn(f) end
        end
    end
    if CompactRaidFrameContainer and CompactRaidFrameContainer.flowFrames then
        for _, f in ipairs(CompactRaidFrameContainer.flowFrames) do
            if f then
                local u = f.displayedUnit or f.unit
                if IsGroupUnit(u) then fn(f) end
            end
        end
    else
        for i = 1, 5 do
            local rf = _G["CompactRaidFrame" .. i]
            local u = rf and (rf.displayedUnit or rf.unit)
            if rf and IsGroupUnit(u) then fn(rf) end
        end
    end
end

local function GetDB()
    local db = AC.DB and AC.DB.profile and AC.DB.profile.partyTrinkets
    if not db then
        db = {
            enabled        = true,
            showOwnTrinket = false,
            onlyShowOnCooldown = false,
            soloRaidFrame  = true,
            iconScale      = 100,
            offsetX        = 0,
            offsetY        = 0,
            style          = "arenacore",
            borderThickness = 2,
            iconDesign     = "retail",
        }
    end
    return db
end

local function AnySoloRaidFrameEnabled()
    local profile = AC.DB and AC.DB.profile
    if not profile then return false end
    return (profile.bigDebuffs and profile.bigDebuffs.soloRaidFrame == true)
        or (profile.partyTrinkets and profile.partyTrinkets.soloRaidFrame == true)
end

local function EnsureTrinketFrame(parent)
    if not parent then return nil end
    local tr = __ptFrames[parent]
    if tr then return tr end

    tr = CreateFrame("Frame", nil, parent)
    tr:EnableMouse(false)
    tr:SetFrameLevel((parent:GetFrameLevel() or 0) + 30)

    tr.icon = tr:CreateTexture(nil, "ARTWORK")
    tr.icon:SetTexCoord(0.08, 0.92, 0.08, 0.92)
    tr.icon:SetAllPoints(tr)

    tr.cooldown = CreateFrame("Cooldown", nil, tr, "CooldownFrameTemplate")
    tr.cooldown:SetAllPoints(tr.icon)
    tr.cooldown:SetReverse(false)
    tr.cooldown:SetDrawEdge(true)
    if tr.cooldown.SetHideCountdownNumbers then
        tr.cooldown:SetHideCountdownNumbers(false)
    end

    -- pixel borders
    tr.borderTop    = tr:CreateTexture(nil, "OVERLAY")
    tr.borderTop:SetTexture("Interface\\Buttons\\WHITE8x8")
    tr.borderTop:SetVertexColor(0, 0, 0, 1)

    tr.borderBottom = tr:CreateTexture(nil, "OVERLAY")
    tr.borderBottom:SetTexture("Interface\\Buttons\\WHITE8x8")
    tr.borderBottom:SetVertexColor(0, 0, 0, 1)

    tr.borderLeft   = tr:CreateTexture(nil, "OVERLAY")
    tr.borderLeft:SetTexture("Interface\\Buttons\\WHITE8x8")
    tr.borderLeft:SetVertexColor(0, 0, 0, 1)

    tr.borderRight  = tr:CreateTexture(nil, "OVERLAY")
    tr.borderRight:SetTexture("Interface\\Buttons\\WHITE8x8")
    tr.borderRight:SetVertexColor(0, 0, 0, 1)

    -- Arena Core / Arena Core Black overlay (same asset as arena trinkets)
    tr.overlay = tr:CreateTexture(nil, "OVERLAY", nil, 1)
    tr.overlay:SetTexCoord(0, 1, 0, 1)
    tr.overlay:Hide()

    tr:Hide()
    __ptFrames[parent] = tr
    return tr
end

local function ApplyBorderStyle(tr, style, thickness)
    style = style or "arenacore"
    thickness = tonumber(thickness) or 2
    if thickness < 1 then thickness = 1 end
    if thickness > 5 then thickness = 5 end

    -- Always hide overlay first; we'll show it if needed
    if tr.overlay then
        tr.overlay:Hide()
    end

    if style == "pixel_black" then
        -- Skill Issue Outline: 4 pixel lines
        for _, border in ipairs({tr.borderTop, tr.borderBottom, tr.borderLeft, tr.borderRight}) do
            border:SetVertexColor(0, 0, 0, 1)
            border:Show()
        end
        tr.borderTop:SetHeight(thickness)
        tr.borderBottom:SetHeight(thickness)
        tr.borderLeft:SetWidth(thickness)
        tr.borderRight:SetWidth(thickness)
    else
        -- Arena Core themes: hide pixel lines, show overlay texture
        for _, border in ipairs({tr.borderTop, tr.borderBottom, tr.borderLeft, tr.borderRight}) do
            border:Hide()
        end

        if tr.overlay then
            if style == "arenacore_black" then
                tr.overlay:SetTexture("Interface\\AddOns\\ArenaCore\\Media\\Classicons\\Overlays\\blackoverlay.tga")
            else
                tr.overlay:SetTexture("Interface\\AddOns\\ArenaCore\\Media\\Classicons\\Overlays\\orangeoverlay.tga")
            end
            tr.overlay:Show()
        end
    end
end

local function LayoutTrinketFrame(tr, parent, db)
    local baseSize = 22
    if parent and parent.GetHeight then
        local h = parent:GetHeight()
        if type(h) == "number" and h > 0 then baseSize = h end
    end

    local scale = db.iconScale or 100
    if scale < 50 then scale = 50 end
    if scale > 300 then scale = 300 end

    local size = math.floor((baseSize * scale / 100) + 0.5)
    if size < 12 then size = 12 end

    tr:SetSize(size, size)
    tr:ClearAllPoints()
    tr:SetPoint("CENTER", parent, "CENTER", db.offsetX or 0, db.offsetY or 0)

    tr.icon:SetAllPoints(tr)
    tr.cooldown:SetAllPoints(tr.icon)

    tr.borderTop:ClearAllPoints()
    tr.borderTop:SetPoint("TOPLEFT", tr, "TOPLEFT", 0, 0)
    tr.borderTop:SetPoint("TOPRIGHT", tr, "TOPRIGHT", 0, 0)

    tr.borderBottom:ClearAllPoints()
    tr.borderBottom:SetPoint("BOTTOMLEFT", tr, "BOTTOMLEFT", 0, 0)
    tr.borderBottom:SetPoint("BOTTOMRIGHT", tr, "BOTTOMRIGHT", 0, 0)

    tr.borderLeft:ClearAllPoints()
    tr.borderLeft:SetPoint("TOPLEFT", tr, "TOPLEFT", 0, 0)
    tr.borderLeft:SetPoint("BOTTOMLEFT", tr, "BOTTOMLEFT", 0, 0)

    tr.borderRight:ClearAllPoints()
    tr.borderRight:SetPoint("TOPRIGHT", tr, "TOPRIGHT", 0, 0)
    tr.borderRight:SetPoint("BOTTOMRIGHT", tr, "BOTTOMRIGHT", 0, 0)

    -- Position overlay slightly larger than frame so its border sits outside the cooldown
    if tr.overlay then
        tr.overlay:ClearAllPoints()
        tr.overlay:SetPoint("TOPLEFT", tr, "TOPLEFT", -2, 2)
        tr.overlay:SetPoint("BOTTOMRIGHT", tr, "BOTTOMRIGHT", 2, -2)
    end

    ApplyBorderStyle(tr, db.style, db.borderThickness)
end

local function UpdateTrinketFrame(parent)
    local db = GetDB()
    local tr = EnsureTrinketFrame(parent)
    if not tr then return end

    local unit = parent and (parent.displayedUnit or parent.unit)

    -- If party trinkets disabled, still allow own trinket when requested
    if not db.enabled then
        if not (unit == "player" and db.showOwnTrinket) then
            tr:Hide()
            return
        end
    end

    -- TEST MODE: bypass all arena/unit checks, show fake cooldown on real compact frames
    if __ptTestActive then
        LayoutTrinketFrame(tr, parent, db)
        tr.icon:SetTexture(TRINKET_ICON)
        tr.cooldown:SetCooldown(GetTime() - 45, 120)
        tr:Show()
        return
    end

    -- Only show in PvP instances (arenas, battlegrounds, solo shuffle)
    -- Hide in dungeons, raids, and other PvE content
    if not IsInPvPInstance() then
        tr:Hide()
        return
    end

    if not unit or not UnitExists(unit) then
        tr:Hide()
        return
    end

    -- Skip own trinket if disabled
    if unit == "player" and db.showOwnTrinket == false then
        tr:Hide()
        return
    end

    -- Show trinket icon always; cooldown starts only after the trinket is used.
    -- This works in prep room too, where the API may not return data yet.

    LayoutTrinketFrame(tr, parent, db)
    tr.icon:SetTexture(TRINKET_ICON)
    RequestTrinketSpell(unit)

    local isOnCooldown = false

    -- Midnight path: use safe duration object API
    if C_PvP.GetArenaCrowdControlDuration and tr.cooldown.SetCooldownFromDurationObject then
        local durationObj = C_PvP.GetArenaCrowdControlDuration(unit)
        if durationObj then
            pcall(tr.cooldown.SetCooldownFromDurationObject, tr.cooldown, durationObj, true)
            isOnCooldown = true
        else
            tr.cooldown:Clear()
        end
    else
        -- Pre-Midnight path: table with startTime/duration
        local info = C_PvP.GetArenaCrowdControlInfo(unit)
        if info and type(info) == "table" then
            local startTime = info.startTime
            local duration  = info.duration
            if type(startTime) == "number" and type(duration) == "number" and duration > 0 then
                tr.cooldown:SetCooldown(startTime, duration)
                isOnCooldown = true
            else
                tr.cooldown:Clear()
            end
        else
            tr.cooldown:Clear()
        end
    end

    -- Only Show When On Cooldown: hide if trinket is available, show if on cooldown
    if db.onlyShowOnCooldown and not isOnCooldown then
        tr:Hide()
        return
    end

    tr:Show()
end

local function RefreshAllTrinketFrames()
    ForEachCompactFrame(UpdateTrinketFrame)
end

local function EnsureEvents()
    if __ptEventFrame then return end
    __ptEventFrame = CreateFrame("Frame")
    __ptEventFrame:RegisterEvent("UNIT_SPELLCAST_SUCCEEDED")
    __ptEventFrame:RegisterEvent("ARENA_COOLDOWNS_UPDATE")
    __ptEventFrame:RegisterEvent("ARENA_CROWD_CONTROL_SPELL_UPDATE")
    __ptEventFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
    __ptEventFrame:RegisterEvent("GROUP_ROSTER_UPDATE")
    __ptEventFrame:SetScript("OnEvent", function(_, event, unit)
        if event == "ARENA_COOLDOWNS_UPDATE" then
            local db = GetDB()
            if db.enabled or db.showOwnTrinket then
                RefreshAllTrinketFrames()
            end
            return
        end
        if event == "ARENA_CROWD_CONTROL_SPELL_UPDATE" then
            if unit and IsGroupUnit(unit) then
                local db = GetDB()
                if db.enabled or (unit == "player" and db.showOwnTrinket) then
                    ForEachCompactFrame(function(f)
                        local u = f.displayedUnit or f.unit
                        if u == unit then
                            UpdateTrinketFrame(f)
                        end
                    end)
                end
            end
            return
        end
        if event == "UNIT_SPELLCAST_SUCCEEDED" then
            -- Quick refresh for this unit if it's a party/raid member
            if unit and IsGroupUnit(unit) then
                local db = GetDB()
                if db.enabled or (unit == "player" and db.showOwnTrinket) then
                    ForEachCompactFrame(function(f)
                        local u = f.displayedUnit or f.unit
                        if u == unit then
                            UpdateTrinketFrame(f)
                        end
                    end)
                end
            end
            return
        end
        -- GROUP_ROSTER_UPDATE / PLAYER_ENTERING_WORLD
        local db = GetDB()
        if not db.enabled and not db.showOwnTrinket then return end
        RefreshAllTrinketFrames()
    end)
end

local function GetPlayerCompactFrame()
    if CompactRaidFrame1 and CompactRaidFrame1.unit == "player" and CompactRaidFrame1:IsShown() then
        return CompactRaidFrame1
    end
    if CompactPartyFrame and CompactPartyFrame.memberUnitFrames then
        for _, f in ipairs(CompactPartyFrame.memberUnitFrames) do
            if f and f.unit == "player" and f:IsShown() then return f end
        end
    end
    return nil
end

-- Solo raid frame support (same pattern as BigDebuffs)
function PT:UpdateSoloRaidFrame()
    if InCombatLockdown and InCombatLockdown() then return end
    if EditModeManagerFrame and EditModeManagerFrame.IsEditModeActive and EditModeManagerFrame:IsEditModeActive() then
        return
    end

    local frame = CompactPartyFrame
    if not frame then return end

    local db = GetDB()
    local solo = not (IsInGroup() or IsInRaid())

    if not db.soloRaidFrame then
        if solo and AnySoloRaidFrameEnabled() then
            if PartyFrame then
                PartyFrame:Show()
                if PartyFrame.HidePartyFrames then
                    PartyFrame:HidePartyFrames()
                end
            end
            frame:Show()
            if PartyFrame and PartyFrame.UpdatePaddingAndLayout then
                PartyFrame:UpdatePaddingAndLayout()
            end
            return
        end

        if solo then
            frame:Hide()
            if PartyFrame and not (EditModeManagerFrame and EditModeManagerFrame.ArePartyFramesForcedShown and EditModeManagerFrame:ArePartyFramesForcedShown()) then
                PartyFrame:Hide()
            end
            if PartyFrame and PartyFrame.UpdatePaddingAndLayout then
                PartyFrame:UpdatePaddingAndLayout()
            end
        elseif frame.UpdateVisibility then
            if PartyFrame and PartyFrame.Show then
                PartyFrame:Show()
                if PartyFrame.UpdatePaddingAndLayout then
                    PartyFrame:UpdatePaddingAndLayout()
                end
            end
            frame:UpdateVisibility()
        end
        return
    end

    if not solo then
        if frame.UpdateVisibility then
            frame:UpdateVisibility()
        else
            frame:Show()
        end
        return
    end

    if PartyFrame then
        PartyFrame:Show()
        if PartyFrame.HidePartyFrames then
            PartyFrame:HidePartyFrames()
        end
    end
    frame:Show()
    if PartyFrame and PartyFrame.UpdatePaddingAndLayout then
        PartyFrame:UpdatePaddingAndLayout()
    end
end

-- ==========================================================================
-- TEST MODE: Show trinkets on real solo raid / party frames for live positioning
-- ==========================================================================

local function DestroyTestFrames()
    __ptTestActive = false

    -- Refresh to hide our overlays using the normal path
    RefreshAllTrinketFrames()

    -- Restore solo raid frame visibility if we forced it
    if __ptTestWasSolo then
        __ptTestWasSolo = false
        -- Let each module re-evaluate frame visibility based on its own settings
        -- instead of blindly hiding everything -- because BigDebuffs might still want the solo raid frame shown
        if AC.BigDebuffs and AC.BigDebuffs.UpdateSoloRaidFrame then
            AC.BigDebuffs:UpdateSoloRaidFrame()
        end
        PT:UpdateSoloRaidFrame()
    end
end

function PT:IsTestActive()
    return __ptTestActive
end

function PT:ToggleTest()
    if __ptTestActive then
        DestroyTestFrames()
        return
    end

    local db = GetDB()
    if not db.enabled and not db.showOwnTrinket then return end

    __ptTestActive = true
    __ptTestWasSolo = not (IsInGroup() or IsInRaid())

    -- If solo, temporarily force the real party frames to show
    if __ptTestWasSolo then
        if PartyFrame then
            PartyFrame:Show()
            if PartyFrame.HidePartyFrames then
                PartyFrame:HidePartyFrames()
            end
        end
        if CompactPartyFrame then
            CompactPartyFrame:Show()
        end
        if PartyFrame and PartyFrame.UpdatePaddingAndLayout then
            PartyFrame:UpdatePaddingAndLayout()
        end
    end

    -- Give Blizzard a moment to create/update member frames, then refresh
    C_Timer.After(0.05, function()
        if not __ptTestActive then return end
        RefreshAllTrinketFrames()
    end)
end

function PT:RefreshTest()
    if not __ptTestActive then return end
    RefreshAllTrinketFrames()
end

function PT:HideTest()
    DestroyTestFrames()
end

-- ==========================================================================
-- PUBLIC API
-- ==========================================================================

function PT:Apply()
    EnsureEvents()
    local testActive = self:IsTestActive()
    RefreshAllTrinketFrames()
    self:UpdateSoloRaidFrame()
    if testActive then
        self:RefreshTest()
    end
end

function PT:Initialize()
    if self.__initialized then return end
    self.__initialized = true

    local ef = CreateFrame("Frame")
    self.eventFrame = ef
    ef:RegisterEvent("PLAYER_ENTERING_WORLD")
    ef:RegisterEvent("GROUP_ROSTER_UPDATE")
    ef:SetScript("OnEvent", function(_, event)
        if event == "PLAYER_ENTERING_WORLD" or event == "GROUP_ROSTER_UPDATE" then
            self:UpdateSoloRaidFrame()
            local db = GetDB()
            if db.enabled or db.showOwnTrinket then
                RefreshAllTrinketFrames()
            end
        end
    end)

    C_Timer.After(0.5, function()
        if PT and PT.Apply then PT:Apply() end
    end)
end
