-------------------------
-- AiB Portals (ALTE FUNKTIONIERENDE VERSION)
-- Popup-Frame an mainFrame, transparent, an Content-Position
-------------------------


local factionGroup = UnitFactionGroup("player")
local siegeID = (factionGroup == "Horde") and 464256 or 445418
local motherID = (factionGroup == "Horde") and 467555 or 467553

----------------------------
-- Portal-Daten (GLOBAL)
----------------------------

-- MIDNIGHT (Expansion 9)
-- SpellIDs von Wowhead Beta
MDT_PL_Midnight = {
    {icon = "Interface\\Icons\\inv_misc_questionmark", spellID = 1254400, name = "WS"},      -- Windrunner Spire
    {icon = "Interface\\Icons\\inv_misc_questionmark", spellID = 1254572, name = "MT"},      -- Magister's Terrace
    {icon = "Interface\\Icons\\inv_misc_questionmark", spellID = 999913, name = "MR"},      -- Murder Row - PLACEHOLDER!
    {icon = "Interface\\Icons\\inv_misc_questionmark", spellID = 999914, name = "DON"},     -- Den of Nalorakk - PLACEHOLDER!
    {icon = "Interface\\Icons\\inv_misc_questionmark", spellID = 1254559, name = "MC"},      -- Maisara Caverns
    {icon = "Interface\\Icons\\inv_misc_questionmark", spellID = 999916, name = "TBV"},     -- The Blinding Vale - PLACEHOLDER!
    {icon = "Interface\\Icons\\inv_misc_questionmark", spellID = 999917, name = "VA"},      -- Voidscar Arena - PLACEHOLDER!
    {icon = "Interface\\Icons\\inv_misc_questionmark", spellID = 1254563, name = "NPX"},     -- Nexus Point Xenas
}

-- SEASON 1 (Midnight Season 1)
MDT_PL_Season1 = {
    {icon = "Interface\\Icons\\inv_misc_questionmark", spellID = 1254572, name = "MT"},      -- Magister's Terrace
    {icon = "Interface\\Icons\\inv_misc_questionmark", spellID = 1254559, name = "MC"},      -- Maisara Caverns
    {icon = "Interface\\Icons\\inv_misc_questionmark", spellID = 1254563, name = "NPX"},     -- Nexus-Point Xenas
    {icon = "Interface\\Icons\\inv_misc_questionmark", spellID = 1254400, name = "WS"},      -- Windrunner Spire
    {icon = "Interface\\Icons\\achievement_dungeon_dragonacademy", spellID = 393273, name = "AA"},  -- Algeth'ar Academy
    {icon = "Interface\\Icons\\inv_misc_questionmark", spellID = 1254551, name = "SOTT"},    -- Seat of the Triumvirate (Dark Dereliction)
    {icon = "Interface\\Icons\\achievement_dungeon_arakkoaspires", spellID = 159898, name = "SR"},  -- Skyreach
    {icon = "Interface\\Icons\\inv_misc_questionmark", spellID = 1254555, name = "POS"},     -- Pit of Saron (Unyielding Blight)
}

MDT_PL_TWW = {
    {icon = "Interface\\Icons\\inv_achievement_dungeon_arak-ara",          spellID = 445417, name = "ARAK"},
    {icon = "Interface\\Icons\\inv_achievement_dungeon_cinderbrewmeadery", spellID = 445440, name = "CM"},
    {icon = "Interface\\Icons\\inv_achievement_dungeon_cityofthreads",     spellID = 445416, name = "COT"},
    {icon = "Interface\\Icons\\inv_achievement_dungeon_darkflamecleft",    spellID = 445441, name = "DFC"},
    {icon = "Interface\\Icons\\inv_achievement_dungeon_dawnbreaker",       spellID = 445414, name = "DAWN"},
    {icon = "Interface\\Icons\\inv_misc_questionmark", spellID = 1237215, name = "ECO"},  -- Eco-Dome Al'dani (Icon vom Spell)
    {icon = "Interface\\Icons\\inv_achievement_dungeon_waterworks",        spellID = 1216786, name = "FLOOD"},
    {icon = "Interface\\Icons\\inv_achievement_dungeon_prioryofthesacredflame", spellID = 445444, name = "PSF"},
    {icon = "Interface\\Icons\\inv_achievement_dungeon_rookery",           spellID = 445443, name = "ROOK"},
    {icon = "Interface\\Icons\\inv_achievement_dungeon_stonevault",        spellID = 445269,  name = "SV"},
}

MDT_PL_DF = {
    {icon = "Interface\\Icons\\achievement_dungeon_dragonacademy",         spellID = 393273, name = "AA"},
    {icon = "Interface\\Icons\\achievement_dungeon_arcanevaults",          spellID = 393279, name = "AV"},
    {icon = "Interface\\Icons\\achievement_dungeon_brackenhidehollow",     spellID = 393267, name = "BH"},
    {icon = "Interface\\Icons\\achievement_dungeon_hallsofinfusion",       spellID = 393283, name = "HOI"},
    {icon = "Interface\\Icons\\achievement_dungeon_neltharus",             spellID = 393276, name = "NELTH"},
    {icon = "Interface\\Icons\\achievement_dungeon_centaurplains",         spellID = 393262, name = "NO"},
    {icon = "Interface\\Icons\\achievement_dungeon_lifepools",             spellID = 393256, name = "RLP"},
    {icon = "Interface\\Icons\\achievement_dungeon_uldaman",               spellID = 393222, name = "ULD"},
    {icon = "Interface\\Icons\\achievement_dungeon_dawnoftheinfinite",     spellID = 424197, name = "DOTI"},
}

MDT_PL_SL = {
    {icon = "Interface\\Icons\\achievement_dungeon_theotherside",          spellID = 354468, name = "DOS"},
    {icon = "Interface\\Icons\\achievement_dungeon_hallsofattonement",     spellID = 354465, name = "HOA"},
    {icon = "Interface\\Icons\\achievement_dungeon_mistsoftirnascithe",    spellID = 354464, name = "MISTS"},
    {icon = "Interface\\Icons\\achievement_dungeon_theneroticwake",        spellID = 354462, name = "NW"},
    {icon = "Interface\\Icons\\achievement_dungeon_plaguefall",            spellID = 354463, name = "PF"},
    {icon = "Interface\\Icons\\achievement_dungeon_sanguinedepths",        spellID = 354469, name = "SD"},
    {icon = "Interface\\Icons\\achievement_dungeon_spireofascension",      spellID = 354466, name = "SOA"},
    {icon = "Interface\\Icons\\achievement_dungeon_theatreofpain",         spellID = 354467, name = "TOP"},
    {icon = "Interface\\Icons\\achievement_dungeon_brokerdungeon",         spellID = 367416, name = "TAZ"},
}

MDT_PL_BFA = {
    {icon = "Interface\\Icons\\achievement_dungeon_ataldazar",             spellID = 424187, name = "AD"},
    {icon = "Interface\\Icons\\achievement_dungeon_freehold",              spellID = 410071, name = "FH"},
    {icon = "Interface\\Icons\\achievement_boss_mechagon",                 spellID = 373274, name = "MECHA"},
    {icon = "Interface\\Icons\\achievement_dungeon_kezan",                 spellID = motherID, name = "ML"},
    {icon = "Interface\\Icons\\achievement_dungeon_siegeofboralus",        spellID = siegeID,  name = "SIEGE"},
    {icon = "Interface\\Icons\\achievement_dungeon_underrot",              spellID = 410074,   name = "UNDR"},
    {icon = "Interface\\Icons\\achievement_dungeon_waycrestmannor",        spellID = 424167,   name = "WM"},
}

MDT_PL_Legion = {
    {icon = "Interface\\Icons\\achievement_dungeon_blackrookhold",         spellID = 424153, name = "BRH"},
    {icon = "Interface\\Icons\\achievement_dungeon_courtofstars",          spellID = 393766, name = "COS"},
    {icon = "Interface\\Icons\\achievement_dungeon_darkheartthicket",      spellID = 424163, name = "DT"},
    {icon = "Interface\\Icons\\achievement_dungeon_hallsofvalor",          spellID = 393764, name = "HOV"},
    {icon = "Interface\\Icons\\achievement_raid_karazhan",                 spellID = 373262, name = "KARA"},
    {icon = "Interface\\Icons\\achievement_dungeon_neltharionslair",       spellID = 410078, name = "NL"},
}

MDT_PL_WOD = {
    {icon = "Interface\\Icons\\achievement_dungeon_auchindoun",           spellID = 159897, name = "AUCH"},
    {icon = "Interface\\Icons\\achievement_dungeon_ogreslagmines",        spellID = 159895, name = "BSM"},
    {icon = "Interface\\Icons\\achievement_dungeon_everbloom",            spellID = 159901, name = "EB"},
    {icon = "Interface\\Icons\\achievement_dungeon_blackrockdepot",       spellID = 159900, name = "GD"},
    {icon = "Interface\\Icons\\achievement_boss_ironjuggernaut",          spellID = 159896, name = "ID"},
    {icon = "Interface\\Icons\\achievement_dungeon_shadowmoonhideout",    spellID = 159899, name = "SBG"},
    {icon = "Interface\\Icons\\achievement_dungeon_arakkoaspires",        spellID = 159898, name = "SR"},
    {icon = "Interface\\Icons\\achievement_dungeon_upperblackrockspire",  spellID = 159902, name = "UBRS"},
}

MDT_PL_MOP = {
    {icon = "Interface\\Icons\\achievement_greatwall",                    spellID = 131225, name = "GOTSS"},
    {icon = "Interface\\Icons\\achievement_dungeon_mogupalace",           spellID = 131222, name = "MSP"},
    {icon = "Interface\\Icons\\spell_holy_senseundead",                   spellID = 131232, name = "SCHOLO"},
    {icon = "Interface\\Icons\\inv_helmet_52",                            spellID = 131231, name = "SH"},
    {icon = "Interface\\Icons\\spell_holy_resurrection",                  spellID = 131229, name = "SM"},
    {icon = "Interface\\Icons\\achievement_dungeon_siegeofniuzaotemple",  spellID = 131228, name = "SNT"},
    {icon = "Interface\\Icons\\achievement_shadowpan_hideout",            spellID = 131206, name = "SPM"},
    {icon = "Interface\\Icons\\achievement_brewery",                      spellID = 131205, name = "SSB"},
    {icon = "Interface\\Icons\\achievement_jadeserpent",                  spellID = 131204, name = "TJS"},
}

MDT_PL_CATA = {
    {icon = "Interface\\Icons\\achievement_dungeon_grimbatol",            spellID = 445424, name = "GB"},
    {icon = "Interface\\Icons\\achievement_dungeon_throne of the tides",  spellID = 424142, name = "TOTT"},
    {icon = "Interface\\Icons\\achievement_dungeon_skywall",              spellID = 410080, name = "VP"},
}

-- RAID PORTALE (Icons werden vom Spell geholt via C_Spell.GetSpellTexture)
MDT_PL_Raids = {
    -- Shadowlands Raids
    {icon = "Interface\\Icons\\inv_misc_questionmark", spellID = 373190, name = "CN"},     -- Castle Nathria
    {icon = "Interface\\Icons\\inv_misc_questionmark", spellID = 373191, name = "SOD"},    -- Sanctum of Domination
    {icon = "Interface\\Icons\\inv_misc_questionmark", spellID = 373192, name = "SFO"},    -- Sepulcher of the First Ones
    -- Dragonflight Raids
    {icon = "Interface\\Icons\\inv_misc_questionmark", spellID = 432254, name = "VOTI"},   -- Vault of the Incarnates
    {icon = "Interface\\Icons\\inv_misc_questionmark", spellID = 432257, name = "ABSC"},   -- Aberrus, the Shadowed Crucible
    {icon = "Interface\\Icons\\inv_misc_questionmark", spellID = 432258, name = "ATDH"},   -- Amirdrassil, the Dream's Hope
    -- The War Within Raids
    {icon = "Interface\\Icons\\inv_misc_questionmark", spellID = 1226482, name = "LOU"},   -- Liberation of Undermine
    {icon = "Interface\\Icons\\inv_misc_questionmark", spellID = 1239155, name = "MFO"},   -- Manaforge Omega
}

----------------------------
-- Cooldown Funktion
----------------------------
local function AddCooldownOverlay(button, spellID, iconTexture, portalName)
    if not button.cooldownText then
        local cdText = button:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        cdText:SetPoint("CENTER", button, "CENTER", 0, 0)
        cdText:SetTextColor(1, 1, 1)
        cdText:SetShadowOffset(1, -1)
        cdText:Hide()
        button.cooldownText = cdText
    end

    if not button.cooldownOverlay then
        local overlay = button:CreateTexture(nil, "OVERLAY")
        overlay:SetAllPoints(true)
        overlay:SetColorTexture(0, 0, 0, 0.4)
        overlay:Hide()
        button.cooldownOverlay = overlay
    end

    button:SetScript("OnUpdate", function(self, elapsed)
        local info = C_Spell.GetSpellCooldown(spellID)
        if not info then
            self.cooldownText:SetText("")
            self.cooldownOverlay:Hide()
            return
        end

        local start = info.startTime or 0
        local duration = info.duration or 0

        if duration > 0 then
            local remaining = duration - (GetTime() - start)
            if remaining > 0 then
                self.cooldownOverlay:Show()
                if remaining >= 3600 then
                    self.cooldownText:SetText(string.format("%dh", math.floor(remaining / 3600)))
                elseif remaining >= 60 then
                    self.cooldownText:SetText(string.format("%dm", math.floor(remaining / 60)))
                else
                    self.cooldownText:SetText(string.format("%ds", math.floor(remaining)))
                end
                self.cooldownText:Show()
            else
                self.cooldownOverlay:Hide()
                self.cooldownText:Hide()
            end
        else
            self.cooldownOverlay:Hide()
            self.cooldownText:Hide()
        end
    end)
end

----------------------------
-- Portal Popup Frame (GENAU wie alte Version!)
----------------------------
AiB_Portals = {}
AiB_Portals.portalPopup = nil -- Mache zugänglich für Reset

function AiB_Portals:CreatePopup(mainFrame, separator)
    if self.portalPopup then return self.portalPopup end
    
    -- Frame an UIParent (NICHT mainFrame - wegen Taint!)
    self.portalPopup = CreateFrame("Frame", "AiB_PortalLibraryPopup", UIParent, "BackdropTemplate")
    self.portalPopup:SetSize(1140, 600)
    self.portalPopup:SetFrameStrata("HIGH")
    
    -- Funktion um Position zu aktualisieren
    local function UpdatePosition()
        if not self.portalPopup:IsShown() then return end
        
        -- Hole mainFrame Position
        local mfLeft = mainFrame:GetLeft() or 0
        local mfTop = mainFrame:GetTop() or 0
        
        -- separator ist bei -265 von mainFrame.TOP
        -- contentFrame ist bei -10 unter separator
        -- Total: -265 - 10 = -275
        local offsetY = -275
        
        self.portalPopup:ClearAllPoints()
        self.portalPopup:SetPoint("TOPLEFT", UIParent, "BOTTOMLEFT",
            mfLeft + 30,  -- +30px nach rechts (war 25)
            mfTop + offsetY)
    end
    
    -- Update bei OnShow
    self.portalPopup:SetScript("OnShow", function(self)
        UpdatePosition()
    end)
    
    -- Update regelmäßig (falls mainFrame bewegt wird)
    self.portalPopup:SetScript("OnUpdate", function(self, elapsed)
        UpdatePosition()
    end)
    
    -- KOMPLETT TRANSPARENTER Hintergrund (unsichtbar!)
    self.portalPopup:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8x8",
        edgeFile = nil,  -- KEIN Rand!
        edgeSize = 0,
        insets = { left = 0, right = 0, top = 0, bottom = 0 },
    })
    self.portalPopup:SetBackdropColor(0, 0, 0, 0) -- KOMPLETT transparent!
    
    self.portalPopup:EnableMouse(true)
    self.portalPopup:Hide()
    
    -- Titel (wie Content-Tab-Titel: Font 20, rot)
    local popupTitle = self.portalPopup:CreateFontString(nil, "OVERLAY")
    popupTitle:SetFont("Fonts\\FRIZQT__.TTF", 20, "OUTLINE")  -- Direkter Font-Pfad
    popupTitle:SetPoint("TOP", self.portalPopup, "TOP", 0, -10)
    popupTitle:SetText("Portal Library")
    popupTitle:SetTextColor(1, 0, 0, 1)  -- ROT (wie andere Titel)
    
    -- ScrollFrame (EXAKT wie alte Version!)
    local scrollFrame = CreateFrame("ScrollFrame", "$parent_ScrollFrame", self.portalPopup)
    scrollFrame:SetSize(self.portalPopup:GetWidth() - 30, self.portalPopup:GetHeight() - 50)
    scrollFrame:SetPoint("TOPLEFT", self.portalPopup, "TOPLEFT", 10, -40)
    
    local scrollChild = CreateFrame("Frame", "$parent_ScrollChild", scrollFrame)
    scrollChild:SetSize(scrollFrame:GetWidth(), 840)
    scrollFrame:SetScrollChild(scrollChild)
    
    -- Expansions (EXAKT wie alte Version!)
    local expansionsData = {
        { expansion="Midnight",         portals=MDT_PL_Midnight },
        { expansion="Season 1",         portals=MDT_PL_Season1 },
        { expansion="The War Within",   portals=MDT_PL_TWW },
        { expansion="Dragonflight",     portals=MDT_PL_DF },
        { expansion="Shadowlands",      portals=MDT_PL_SL },
        { expansion="Battle for Azeroth", portals=MDT_PL_BFA },
        { expansion="Legion",           portals=MDT_PL_Legion },
        { expansion="Warlords of Draenor", portals=MDT_PL_WOD },
        { expansion="Mists of Pandaria", portals=MDT_PL_MOP },
        { expansion="Cataclysm",        portals=MDT_PL_CATA },
        { expansion="Raids",            portals=MDT_PL_Raids },
    }
    
    local buttonSize = 35  -- REDUZIERT von 40
    local buttonSpacing = 10  -- ERHÖHT von 6 für besseren Label-Abstand
    local frameWidth = scrollFrame:GetWidth()
    
    local yPos = 10
    
    -- ========== SEASON 1 - ZENTRAL OBEN ==========
    local season1 = expansionsData[2]  -- Index 2 weil Midnight ist 1
    
    -- Season 1 Titel (zentral)
    local s1Title = scrollChild:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    s1Title:SetPoint("TOP", scrollChild, "TOP", 0, -yPos)
    s1Title:SetText(season1.expansion)
    s1Title:SetTextColor(1, 0.8, 0)  -- Gold
    s1Title:SetFont("Fonts\\FRIZQT__.TTF", 16, "OUTLINE")
    
    yPos = yPos + 22  -- REDUZIERT von 25
    
    -- Season 1 Buttons (zentral)
    local numS1 = #season1.portals
    local s1Width = numS1 * (buttonSize + buttonSpacing) - buttonSpacing
    local s1StartX = (frameWidth - s1Width) / 2
    
    -- Season 1 Portale - ERST FILTERN für korrekte Zentrierung
    local hideUnlearned = AiB_Settings.hide_unlearned_portals or false
    local visibleSeason1 = {}
    
    for _, portal in ipairs(season1.portals) do
        local known = IsSpellKnown(portal.spellID)
        if not hideUnlearned or known then
            table.insert(visibleSeason1, {portal = portal, known = known})
        end
    end
    
    -- Zentriere basierend auf SICHTBAREN Portalen
    local numVisible = #visibleSeason1
    local s1Width = numVisible * buttonSize + (numVisible - 1) * buttonSpacing
    local s1StartX = (frameWidth - s1Width) / 2
    
    for i, data in ipairs(visibleSeason1) do
        local portal = data.portal
        local known = data.known
        local xPos = s1StartX + (i-1) * (buttonSize + buttonSpacing)
        
        local btn = CreateFrame("Button", nil, scrollChild, "SecureActionButtonTemplate")
        btn:SetSize(buttonSize, buttonSize)
        btn:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", xPos, -yPos)
        
        local icon = btn:CreateTexture(nil, "ARTWORK")
        icon:SetAllPoints(true)
        
        local hoverOverlay = btn:CreateTexture(nil, "HIGHLIGHT")
        hoverOverlay:SetAllPoints(btn)
        hoverOverlay:SetTexture("Interface\\AddOns\\aib\\textures\\aib_button_highlight.blp")
        hoverOverlay:SetBlendMode("ADD")
        hoverOverlay:SetVertexColor(1, 1, 1, 0.25)
        
        if known then
            -- Hole Icon vom Spell (dynamisch)
            local spellTexture = C_Spell.GetSpellTexture(portal.spellID)
            icon:SetTexture(spellTexture or portal.icon or 134400)
            btn:Enable()
            btn:RegisterForClicks("LeftButtonUp", "LeftButtonDown")
            btn:SetAttribute("type", "spell")
            btn:SetAttribute("unit", "player")
            btn:SetAttribute("spell", portal.spellID)
            AddCooldownOverlay(btn, portal.spellID, icon, portal.name)
            btn:SetScript("OnEnter", function(self)
                GameTooltip:SetOwner(self, "ANCHOR_TOP")
                GameTooltip:SetSpellByID(portal.spellID)
                GameTooltip:Show()
            end)
            btn:SetScript("OnLeave", function(self) GameTooltip:Hide() end)
        else
            icon:SetTexture("Interface\\AddOns\\aib\\textures\\aib_lock.blp")
            icon:SetVertexColor(1, 1, 1, 0.8)
            btn:Disable()
            btn:SetScript("OnEnter", function(self)
                GameTooltip:SetOwner(self, "ANCHOR_TOP")
                GameTooltip:AddLine(portal.name, 1,1,1)
                GameTooltip:AddLine("Portal not learned yet.", 1,0,0)
                GameTooltip:Show()
            end)
            btn:SetScript("OnLeave", function(self) GameTooltip:Hide() end)
        end
        
        local label = scrollChild:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        label:SetPoint("TOP", btn, "BOTTOM", 0, -2)
        label:SetText(portal.name)
        label:SetTextColor(1, 1, 1, 1)
    end
    
    yPos = yPos + buttonSize + 20  -- REDUZIERT von 30
    
    -- ========== REST - LINKS/RECHTS ABWECHSELND ==========
    local leftColumn = true
    local maxButtonsPerRow = 10  -- ERHÖHT von 9 auf 10
    local leftYPos = yPos  -- Separate Y für links
    local rightYPos = yPos  -- Separate Y für rechts
    
    for idx = 1, #expansionsData do
        -- Skip Season 1 (Index 2 - schon zentral gemacht)
        if idx ~= 2 then
            local expData = expansionsData[idx]
        local expData = expansionsData[idx]
        local numPortals = #expData.portals
        
        -- Zähle SICHTBARE Portale (wichtig für rechtsbündige Spalte!)
        local hideUnlearned = AiB_Settings.hide_unlearned_portals or false
        local visiblePortals = 0
        if hideUnlearned then
            for _, portal in ipairs(expData.portals) do
                if IsSpellKnown(portal.spellID) then
                    visiblePortals = visiblePortals + 1
                end
            end
        else
            visiblePortals = numPortals
        end
        
        -- Verwende die richtige Y-Position je nach Spalte
        local currentY = leftColumn and leftYPos or rightYPos
        
        -- Titel
        local expTitle = scrollChild:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        expTitle:SetText(expData.expansion)
        
        -- Midnight bekommt Lila, Rest bekommt Cyan
        if idx == 1 then
            expTitle:SetTextColor(0.6, 0.3, 1)  -- Lila für Midnight
        else
            expTitle:SetTextColor(0.2, 0.8, 0.9)  -- Cyan für Rest
        end
        
        expTitle:SetFont("Fonts\\FRIZQT__.TTF", 14, "OUTLINE")
        
        local xStart
        if leftColumn then
            -- LINKS
            expTitle:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", 10, -currentY)
            xStart = 10
        else
            -- RECHTS - Titel rechtsbündig
            expTitle:SetPoint("TOPRIGHT", scrollChild, "TOPRIGHT", -10, -currentY)
            -- Buttons von rechts nach links - BASIEREND AUF SICHTBAREN PORTALEN!
            local groupWidth = math.min(visiblePortals, maxButtonsPerRow) * (buttonSize + buttonSpacing) - buttonSpacing
            xStart = frameWidth - 10 - groupWidth
        end
        
        currentY = currentY + 18  -- REDUZIERT von 20
        
        -- Buttons für diese Gruppe
        local xPos = xStart
        local rowY = currentY
        local buttonsInRow = 0
        
        for i, portal in ipairs(expData.portals) do
            local known = IsSpellKnown(portal.spellID)
            
            -- Prüfe ob ungelernte Portale ausgeblendet werden sollen
            local hideUnlearned = AiB_Settings.hide_unlearned_portals or false
            
            -- Erstelle Button nur wenn: nicht hidden ODER known
            if not hideUnlearned or known then
                local btn = CreateFrame("Button", nil, scrollChild, "SecureActionButtonTemplate")
                btn:SetSize(buttonSize, buttonSize)
                btn:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", xPos, -rowY)
                
                local icon = btn:CreateTexture(nil, "ARTWORK")
                icon:SetAllPoints(true)
                
                local hoverOverlay = btn:CreateTexture(nil, "HIGHLIGHT")
                hoverOverlay:SetAllPoints(btn)
                hoverOverlay:SetTexture("Interface\\AddOns\\aib\\textures\\aib_button_highlight.blp")
                hoverOverlay:SetBlendMode("ADD")
                hoverOverlay:SetVertexColor(1, 1, 1, 0.25)
                
                if known then
                    -- Hole Icon vom Spell (dynamisch)
                    local spellTexture = C_Spell.GetSpellTexture(portal.spellID)
                    icon:SetTexture(spellTexture or portal.icon or 134400)
                    btn:Enable()
                    btn:RegisterForClicks("LeftButtonUp", "LeftButtonDown")
                    btn:SetAttribute("type", "spell")
                    btn:SetAttribute("unit", "player")
                    btn:SetAttribute("spell", portal.spellID)
                    AddCooldownOverlay(btn, portal.spellID, icon, portal.name)
                    btn:SetScript("OnEnter", function(self)
                        GameTooltip:SetOwner(self, "ANCHOR_TOP")
                        GameTooltip:SetSpellByID(portal.spellID)
                        GameTooltip:Show()
                    end)
                    btn:SetScript("OnLeave", function(self) GameTooltip:Hide() end)
                else
                    icon:SetTexture("Interface\\AddOns\\aib\\textures\\aib_lock.blp")
                    icon:SetVertexColor(1, 1, 1, 0.8)
                    btn:Disable()
                    btn:SetScript("OnEnter", function(self)
                        GameTooltip:SetOwner(self, "ANCHOR_TOP")
                        GameTooltip:AddLine(portal.name, 1,1,1)
                        GameTooltip:AddLine("Portal not learned yet.", 1,0,0)
                        GameTooltip:Show()
                    end)
                    btn:SetScript("OnLeave", function(self) GameTooltip:Hide() end)
                end
            
            local label = scrollChild:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
            label:SetPoint("TOP", btn, "BOTTOM", 0, -2)
            label:SetText(portal.name)
            label:SetTextColor(1, 1, 1, 1)
            
            xPos = xPos + buttonSize + buttonSpacing
            buttonsInRow = buttonsInRow + 1
            
            -- Neue Zeile nach maxButtonsPerRow
            if buttonsInRow >= maxButtonsPerRow and i < numPortals then
                xPos = xStart
                rowY = rowY + buttonSize + 18  -- REDUZIERT von 25
                buttonsInRow = 0
            end
            end -- Ende if not hideUnlearned or known
        end -- Ende for i, portal in ipairs(expData.portals)
        
        -- Speichere die finale Y-Position für diese Spalte
        local finalY = rowY + buttonSize + 25  -- REDUZIERT von 35
        if leftColumn then
            leftYPos = finalY
        else
            rightYPos = finalY
        end
        
        -- Wechsle Seite
        leftColumn = not leftColumn
        
            -- Wenn beide Spalten fertig sind, nimm die größere Y-Position
            if leftColumn then
                -- Nach rechts-Gruppe -> beide Spalten fertig
                yPos = math.max(leftYPos, rightYPos)
                leftYPos = yPos
                rightYPos = yPos
            end
        end  -- Ende von if idx ~= 2
    end
    
    -- FIX: Popup zu UISpecialFrames hinzufügen damit ESC ihn direkt
    --      über den secure Code path schließen kann (kein Addon-Taint).
    table.insert(UISpecialFrames, "AiB_PortalLibraryPopup")

    return self.portalPopup
end

function AiB_Portals:Show()
    if self.portalPopup then
        self.portalPopup:Show()
    end
end

-- FIX: InCombatLockdown()-Guard verhindert ADDON_ACTION_BLOCKED wenn
--      Hide() aus einem tainted/protected Kontext heraus aufgerufen wird.
--      Im Kampf wird der Popup über UISpecialFrames (ESC) sicher geschlossen.
function AiB_Portals:Hide()
    if self.portalPopup then
        if InCombatLockdown() then
            -- Deferred hide - escape the protected call stack
            C_Timer.After(0, function()
                if self.portalPopup and self.portalPopup:IsShown() then
                    self.portalPopup:Hide()
                end
            end)
        else
            self.portalPopup:Hide()
        end
    end
end

function AiB_Portals:Toggle()
    if self.portalPopup then
        if self.portalPopup:IsShown() then
            self:Hide()  -- nutzt den gesicherten Hide()-Pfad oben
        else
            self.portalPopup:Show()
        end
    end
end

-- Reset Funktion (für Settings)
function AiB_Portals:ResetPopup()
    if self.portalPopup then
        if not InCombatLockdown() then
            self.portalPopup:Hide()
        end
    end
    self.portalPopup = nil
end

