local ADDON_NAME, NS = ...
local L = LibStub("AceLocale-3.0"):GetLocale("MDungeonTeleports")

local MDTWorldMapTab = {}
MDTWorldMapTab.tabSpacing = 4
local DISPLAY_MODE = "MDT_PEEPO"

-- MARK: Check if the tab is enabled
function MDTWorldMapTab:IsEnabled()
    return not MDungeonTeleports or MDungeonTeleports.worldMapTab ~= false
end

-- MARK: Initialize the tab button
function MDTWorldMapTab:Initialize()
    if self.initialized then return end

    if not self:IsEnabled() then
        return
    end
    
    if not WorldMapFrame or not WorldMapFrame.BorderFrame then
        -- If WorldMapFrame doesn't exist yet, delay initialization
        C_Timer.After(0.5, function() self:Initialize() end)
        return
    end
    
    -- Create a side tab button that matches the Quests/Events/MapLegend tabs
    local tabParent = QuestMapFrame or WorldMapFrame.BorderFrame
    self.button = CreateFrame("Button", "MDTWorldMapTabButton", tabParent, "LargeSideTabButtonTemplate")
    self.button:SetFrameStrata("HIGH")
    self.button:SetFrameLevel(WorldMapFrame.BorderFrame:GetFrameLevel() + 5)

    -- Set up button properties
    self.button.tooltipText = L and L["TELEPORTS_TITLE"] or "M+ Dungeon Teleports"
    self.button.displayMode = DISPLAY_MODE
    self.button.activeAtlas = "Interface\\AddOns\\MDungeonTeleports\\media\\ui\\map_icon_active.blp"
    self.button.inactiveAtlas = "Interface\\AddOns\\MDungeonTeleports\\media\\ui\\map_icon_inactive.blp"
    self:SetupButtonVisuals()

    if not self.button.mdtIconHooked then
        self.button:HookScript("OnShow", function()
            MDTWorldMapTab:SetupButtonVisuals()
        end)
        self.button.mdtIconHooked = true
    end
    self:ApplyElvUISkin()
    self.button:SetChecked(false)

    -- Use mixin click handler to select display mode
    self.button:SetCustomOnMouseUpHandler(function(btn, mouseButton, upInside)
        if mouseButton == "LeftButton" and upInside then
            self:SelectTab()
        end
    end)

    -- Anchor to the existing side tabs on the right
    self:AnchorButton()
    self:HookOtherTabs()
    self:HookOtherContentFrames()
    self:HookDisplayMode()
    
    -- Make button visible
    self.button:Show()
    
    -- Create the content panel
    self:CreatePanel()
    -- Do not inject into QuestMapFrame content frames to avoid protected SetShown calls in combat
    
    self.initialized = true

    if QuestMapFrame and QuestMapFrame.displayMode == DISPLAY_MODE then
        self:ShowPanel()
    end
end

-- MARK: Apply ElvUI skin to the tab button
function MDTWorldMapTab:ApplyElvUISkin()
    if not self.button then
        return
    end
    if not C_AddOns.IsAddOnLoaded("ElvUI") then
        return
    end
    if not ElvUI then
        return
    end

    local ok, E = pcall(unpack, ElvUI)
    if not ok or not E or not E.GetModule then
        return
    end

    local blizzardSkins = E.private and E.private.skins and E.private.skins.blizzard
    if blizzardSkins and blizzardSkins.worldmap == false then
        return
    end

    local S = E:GetModule("Skins", true)
    if not S then
        return
    end

    if S.HandleTab then
        S:HandleTab(self.button)
    elseif S.HandleButton then
        S:HandleButton(self.button)
    end

    self:SetupButtonVisuals()

    if not self.button.mdtCheckedHighlight then
        local checkedHighlight = self.button:CreateTexture(nil, "OVERLAY")
        checkedHighlight:ClearAllPoints()
        checkedHighlight:SetDrawLayer("BACKGROUND", 0)
        checkedHighlight:SetPoint("TOPLEFT", self.button, "TOPLEFT", 4, -2)
        checkedHighlight:SetPoint("BOTTOMRIGHT", self.button, "BOTTOMRIGHT", -4, 4)
        checkedHighlight:SetColorTexture(1, 0.82, 0, 0.25)
        checkedHighlight:Hide()
        self.button.mdtCheckedHighlight = checkedHighlight

        local function UpdateCheckedHighlight(checked)
            local isChecked = checked
            if isChecked == nil and self.button.GetChecked then
                isChecked = self.button:GetChecked()
            end
            if isChecked == nil and self.button.SelectedTexture and self.button.SelectedTexture.IsShown then
                isChecked = self.button.SelectedTexture:IsShown()
            end
            if isChecked then
                self.button.mdtCheckedHighlight:Show()
            else
                self.button.mdtCheckedHighlight:Hide()
            end
        end

        hooksecurefunc(self.button, "SetChecked", function(_, checked)
            UpdateCheckedHighlight(checked)
        end)
        self.button:HookScript("OnShow", function()
            UpdateCheckedHighlight(nil)
        end)
        UpdateCheckedHighlight(nil)
    end

    if not self.button.mdtHoverHighlight then
        local highlight = self.button:GetHighlightTexture()
        if not highlight then
            highlight = self.button:CreateTexture(nil, "HIGHLIGHT")
        end
        highlight:ClearAllPoints()
        highlight:SetPoint("TOPLEFT", self.button, "TOPLEFT", 4, -2)
        highlight:SetPoint("BOTTOMRIGHT", self.button, "BOTTOMRIGHT", -4, 4)
        highlight:SetColorTexture(1, 1, 1, 0.15)
        highlight:SetBlendMode("ADD")
        self.button.mdtHoverHighlight = highlight
    end

    self.tabSpacing = 1
    if self.button then
        self:AnchorButton()
    end

    self.button:SetSize(38, 46)
end

-- MARK: Set up button visuals to match LargeSideTabButtonTemplate
function MDTWorldMapTab:SetupButtonVisuals()
    if self.button and self.button.Icon then
        if not self.button.mdtApplyIconLayout then
            self.button.mdtApplyIconLayout = function()
                if not MDTWorldMapTab.button or not MDTWorldMapTab.button.Icon then
                    return
                end
                local icon = MDTWorldMapTab.button.Icon
                local offsetX = C_AddOns.IsAddOnLoaded("ElvUI") and 0 or -2
                icon:ClearAllPoints()
                icon:SetPoint("CENTER", MDTWorldMapTab.button, "CENTER", offsetX, 0)
                icon:SetSize(20, 20)
            end
        end

        self.button.Icon:SetTexture(self.button.inactiveAtlas)
        self.button.mdtApplyIconLayout()

        if not self.button.mdtIconPositionHooked then
            self.button:HookScript("OnMouseDown", function()
                MDTWorldMapTab.button.mdtApplyIconLayout()
            end)
            self.button:HookScript("OnMouseUp", function()
                MDTWorldMapTab.button.mdtApplyIconLayout()
            end)
            self.button:HookScript("OnClick", function()
                MDTWorldMapTab.button.mdtApplyIconLayout()
            end)
            self.button.mdtIconPositionHooked = true
        end
    end
end

-- MARK: Show the content panel, display mode
function MDTWorldMapTab:SelectTab()
    if not self:IsEnabled() then
        return
    end

    if InCombatLockdown() then
        self:ShowPanel()
        return
    end

    if QuestMapFrame and QuestMapFrame.SetDisplayMode then
        QuestMapFrame:SetDisplayMode(DISPLAY_MODE)
        return
    end

    self:ShowPanel()
end

-- MARK: parent
function MDTWorldMapTab:GetPanelParent()
    if QuestMapFrame then
        return QuestMapFrame
    end
    if WorldMapFrame and WorldMapFrame.QuestLog then
        return WorldMapFrame.QuestLog
    end
    return WorldMapFrame
end

-- MARK: content anchor
function MDTWorldMapTab:GetContentAnchor()
    if QuestMapFrame then
        if QuestMapFrame.ContentsAnchor then
            return QuestMapFrame.ContentsAnchor
        end
        if QuestMapFrame.MapLegendFrame then
            return QuestMapFrame.MapLegendFrame
        end
        if QuestMapFrame.EventsFrame then
            return QuestMapFrame.EventsFrame
        end
        if QuestMapFrame.QuestsFrame then
            return QuestMapFrame.QuestsFrame
        end
    end
    return self:GetPanelParent()
end

-- MARK: Find other side tabs to hook into
function MDTWorldMapTab:FindSideTabs()
    local tabs = {}
    local function AddIfTab(frame)
        if frame and frame.Background and frame.Icon and frame.SetChecked then
            if frame.Background.GetAtlas and frame.Background:GetAtlas() == "questlog-tab-side" then
                table.insert(tabs, frame)
            end
        end
    end

    if QuestMapFrame then
        AddIfTab(QuestMapFrame.QuestsTab)
        AddIfTab(QuestMapFrame.EventsTab)
        AddIfTab(QuestMapFrame.MapLegendTab)
    end

    local function Scan(frame)
        if not frame or not frame.GetChildren then
            return
        end
        for _, child in ipairs({ frame:GetChildren() }) do
            if child ~= self.button then
                AddIfTab(child)
            end
            Scan(child)
        end
    end

    Scan(WorldMapFrame)
    return tabs
end

-- MARK: Anchor the tab button
function MDTWorldMapTab:AnchorButton()
    if InCombatLockdown() then
        return
    end
    local anchorTab = (QuestMapFrame and QuestMapFrame.MapLegendTab) or (QuestMapFrame and QuestMapFrame.EventsTab) or (QuestMapFrame and QuestMapFrame.QuestsTab)

    self.button:ClearAllPoints()
    if anchorTab then
        self.button:SetPoint("TOP", anchorTab, "BOTTOM", 0, -(self.tabSpacing or 4))
    else
        self.button:SetPoint("TOPRIGHT", WorldMapFrame.BorderFrame, "TOPRIGHT", -8, -100)
    end
end

-- MARK: Hook other side tabs to hide panel on click
function MDTWorldMapTab:HookOtherTabs()
    local tabs = self:FindSideTabs()
    for _, tab in ipairs(tabs) do
        if not tab.mdtHooked then
            if tab.SetCustomOnMouseUpHandler then
                local existingHandler = tab.customMouseUpHandler
                tab:SetCustomOnMouseUpHandler(function(btn, mouseButton, upInside)
                    if existingHandler then
                        existingHandler(btn, mouseButton, upInside)
                    end
                    if upInside and mouseButton == "LeftButton" then
                        MDTWorldMapTab:HidePanel()
                    end
                end)
            end

            if tab.HookScript then
                tab:HookScript("OnMouseUp", function(_, mouseButton)
                    if mouseButton == "LeftButton" then
                        MDTWorldMapTab:HidePanel()
                    end
                end)
            end

            tab.mdtHooked = true
        end
    end
end

-- MARK: Hook display mode changes
function MDTWorldMapTab:HookDisplayMode()
    if not QuestMapFrame then
        return
    end

    if QuestMapFrame.SetDisplayMode and not QuestMapFrame.mdtDisplayModeHooked then
        hooksecurefunc(QuestMapFrame, "SetDisplayMode", function(_, mode)
            if mode == DISPLAY_MODE then
                MDTWorldMapTab:ShowPanel()
            else
                MDTWorldMapTab:HidePanel()
            end
        end)
        QuestMapFrame.mdtDisplayModeHooked = true
    end
end

-- MARK: Inject panel and button into QuestMapFrame content frames
function MDTWorldMapTab:InjectIntoQuestMapFrame()
    if not QuestMapFrame or not self.panel or not self.button then
        return
    end

    if QuestMapFrame.ContentFrames then
        local exists = false
        for _, frame in ipairs(QuestMapFrame.ContentFrames) do
            if frame == self.panel then
                exists = true
                break
            end
        end
        if not exists then
            table.insert(QuestMapFrame.ContentFrames, self.panel)
        end
    end

    if QuestMapFrame.TabButtons then
        local exists = false
        for _, tab in ipairs(QuestMapFrame.TabButtons) do
            if tab == self.button then
                exists = true
                break
            end
        end
        if not exists then
            table.insert(QuestMapFrame.TabButtons, self.button)
        end
    end

    if QuestMapFrame.ValidateTabs then
        QuestMapFrame:ValidateTabs()
    end
end

-- MARK: Hook other content frames to hide panel on show
function MDTWorldMapTab:HookOtherContentFrames()
    if not QuestMapFrame then
        return
    end

    local function HookFrame(frame)
        if frame and not frame.mdtHooked then
            hooksecurefunc(frame, "Show", function()
                MDTWorldMapTab:HidePanel()
            end)
            frame.mdtHooked = true
        end
    end

    HookFrame(QuestMapFrame.QuestsFrame)
    HookFrame(QuestMapFrame.EventsFrame)
    HookFrame(QuestMapFrame.MapLegendFrame)
end

-- MARK: Create the main content panel
function MDTWorldMapTab:CreatePanel()
    local panelParent = self:GetPanelParent()
    local contentAnchor = self:GetContentAnchor()
    self.panel = CreateFrame("Frame", "MDTWorldMapPanel", panelParent)
    self.panel:SetAllPoints(contentAnchor)
    self.panel:SetFrameStrata(contentAnchor:GetFrameStrata())
    self.panel:SetFrameLevel(contentAnchor:GetFrameLevel() + 10)
    self.panel:EnableMouse(true)
    self.panel:Hide()

    if QuestMapFrame and contentAnchor == QuestMapFrame.ContentsAnchor then
        self.panel:ClearAllPoints()
        self.panel:SetPoint("TOPLEFT", contentAnchor, "TOPLEFT", 0, -29)
        self.panel:SetPoint("BOTTOMRIGHT", contentAnchor, "BOTTOMRIGHT", -22, 0)
    end

    -- Create scroll frame (MapLegend-style)
    local scrollFrame = CreateFrame("ScrollFrame", "MDTWorldMapPanelScrollFrame", self.panel, "ScrollFrameTemplate")
    scrollFrame:ClearAllPoints()
    scrollFrame:SetPoint("TOPLEFT")
    scrollFrame:SetPoint("BOTTOMRIGHT")
    self.panel.ScrollFrame = scrollFrame

    -- Background inset to reveal border artwork
    local bg = scrollFrame:CreateTexture(nil, "BACKGROUND")
    bg:SetAtlas("QuestLog-main-background", true)
    bg:ClearAllPoints()
    bg:SetPoint("TOPLEFT", scrollFrame, "TOPLEFT", 3, -1)
    bg:SetPoint("BOTTOMRIGHT", scrollFrame, "BOTTOMRIGHT", -3, 0)
    scrollFrame.Background = bg

    -- Scrollbar alignment similar to MapLegend
    if scrollFrame.ScrollBar then
        scrollFrame.ScrollBar:ClearAllPoints()
        scrollFrame.ScrollBar:SetPoint("TOPLEFT", scrollFrame, "TOPRIGHT", 8, 2)
        scrollFrame.ScrollBar:SetPoint("BOTTOMLEFT", scrollFrame, "BOTTOMRIGHT", 8, -4)
    end

    -- Scroll child
    local scrollChild = CreateFrame("Frame", nil, scrollFrame)
    scrollChild:SetSize(1, 1)
    scrollFrame:SetScrollChild(scrollChild)
    self.panel.ScrollFrame.ScrollChild = scrollChild

    -- Border Frame around the scroll area
    local borderFrame = CreateFrame("Frame", nil, self.panel, "QuestLogBorderFrameTemplate")
    borderFrame:ClearAllPoints()
    borderFrame:SetPoint("TOPLEFT", scrollFrame, "TOPLEFT", -3, 7)
    borderFrame:SetPoint("BOTTOMRIGHT", scrollFrame, "BOTTOMRIGHT", 3, -6)
    self.panel.BorderFrame = borderFrame

    -- Title
    local titleText = self.panel:CreateFontString(nil, "ARTWORK", "GameFontNormal")
    titleText:SetPoint("BOTTOM", borderFrame, "TOP", -1, 3)
    local fontPath, fontSize, fontFlags = GameFontNormal:GetFont()
    if fontPath and fontSize then
        titleText:SetFont(fontPath, fontSize + 3, fontFlags)
    end
    titleText:SetTextColor(1, 1, 1)
    titleText:SetText(L["TELEPORTS_TITLE"])
    self.panel.TitleText = titleText

    self:BuildScrollContent()
end

local function GetSpellIcon(spellID)
    if not spellID then return "Interface\\Icons\\INV_Misc_QuestionMark" end
    local spellInfo = C_Spell.GetSpellInfo(spellID)
    if spellInfo and spellInfo.iconID then
        return spellInfo.iconID
    end
    return "Interface\\Icons\\INV_Misc_QuestionMark"
end

local function GetSpellName(spellID)
    if not spellID then return nil end
    local spellInfo = C_Spell.GetSpellInfo(spellID)
    return spellInfo and spellInfo.name or nil
end

local function GetItemIcon(itemID)
    if not itemID then return "Interface\\Icons\\INV_Misc_QuestionMark" end
    local _, _, _, _, icon = C_Item.GetItemInfoInstant(itemID)
    return icon or "Interface\\Icons\\INV_Misc_QuestionMark"
end

local function GetItemName(itemID)
    if not itemID then return nil end
    if C_Item and C_Item.GetItemNameByID then
        local name = C_Item.GetItemNameByID(itemID)
        if name and name ~= "" then
            return name
        end
    end
    local name = GetItemInfo(itemID)
    return name
end

local function IsSpellKnown(spellID)
    if not spellID then return false end
    return C_SpellBook.IsSpellInSpellBook(spellID)
end

local function IsSpellKnownOrUsable(spellID)
    if not spellID then return false end
    if C_Spell and C_Spell.IsSpellKnown then
        return C_Spell.IsSpellKnown(spellID)
    end
    return IsSpellKnown(spellID)
end

local function IsSpellKnownForDisplay(spellID)
    if not spellID then return false end
    if spellID == 1233637 and C_Spell and C_Spell.IsSpellUsable then
        local usable = C_Spell.IsSpellUsable(spellID)
        if usable then
            return true
        end
    end
    return IsSpellKnownOrUsable(spellID)
end

local function IsItemKnown(itemID)
    if not itemID then return false end
    if C_ToyBox.GetToyInfo(itemID) then
        return PlayerHasToy(itemID)
    end
    return (C_Item.GetItemCount(itemID) or 0) > 0
end

local function IsToyKnown(itemID)
    if not itemID then return false end
    return PlayerHasToy(itemID)
end

local function NormalizeHearthstones(list, selectedItemID, preferIcon)
    local entries = {}
    local fallback = nil
    for _, data in ipairs(list or {}) do
        local itemID = tonumber(data.itemID)
        if itemID then
            local isToy = C_ToyBox and C_ToyBox.GetToyInfo and C_ToyBox.GetToyInfo(itemID)
            local itemName = GetItemName(itemID) or data.name
            local itemIcon = GetItemIcon(itemID)
            local entry = {
                name = itemName,
                itemID = itemID,
                actionType = isToy and "toy" or "item",
                icon = itemIcon,
                preferIcon = false
            }
            if not fallback then
                fallback = entry
            end
            if selectedItemID and itemID == selectedItemID then
                return { entry }
            end
            table.insert(entries, entry)
        end
    end
    if selectedItemID then
        return fallback and { fallback } or {}
    end
    return entries
end

local function NormalizeToyList(list, preferIcon)
    local entries = {}
    for _, data in ipairs(list or {}) do
        local itemID = tonumber(data.toyID) or tonumber(data.name)
        local name = data.name
        if tonumber(data.name) and data.toyID then
            name = data.toyID
        end
        if itemID then
            local itemName = GetItemName(itemID) or name
            local itemIcon = GetItemIcon(itemID)
            table.insert(entries, {
                name = itemName,
                itemID = itemID,
                actionType = "toy",
                icon = itemIcon,
                preferIcon = false
            })
        end
    end
    return entries
end

local function NormalizeSpellList(list, preferIcon)
    local entries = {}
    for _, data in ipairs(list or {}) do
        local spellID = tonumber(data.spellID)
        if spellID then
            local spellName = GetSpellName(spellID) or data.name
            local spellIcon = GetSpellIcon(spellID)
            table.insert(entries, {
                name = spellName,
                spellID = spellID,
                actionType = "spell",
                icon = spellIcon,
                preferIcon = false
            })
        end
    end
    return entries
end

local function NormalizePortalList(list)
    local entries = {}
    for _, data in ipairs(list or {}) do
        local spellID = tonumber(data.spellID)
        if spellID then
            local spellName = GetSpellName(spellID) or data.fullname or data.name
            local spellIcon = GetSpellIcon(spellID)
            table.insert(entries, {
                name = spellName,
                spellID = spellID,
                actionType = "spell",
                icon = spellIcon
            })
        end
    end
    return entries
end

local function NormalizePortalListTableData(list)
    local entries = {}
    for _, data in ipairs(list or {}) do
        local spellID = tonumber(data.spellID)
        if spellID then
            table.insert(entries, {
                name = data.fullname or data.name,
                spellID = spellID,
                actionType = "spell",
                icon = data.icon,
                preferIcon = true,
                useTableData = true
            })
        end
    end
    return entries
end

-- MARK: Time Formatting
local function FormatCooldownTime(seconds)
    if seconds >= 3600 then
        local hours = math.floor(seconds / 3600)
        return string.format("%dh", hours)
    elseif seconds >= 60 then
        local minutes = math.floor(seconds / 60)
        return string.format("%dm", minutes)
    else
        return string.format("%ds", math.floor(seconds))
    end
end

-- MARK: Cooldown Text Function
local function UpdateCooldownText(button, entry)
    if InCombatLockdown() then return end

    local cooldownFrame = button.cooldownText
    local cooldownOverlay = button.cooldownOverlay
    local startTime
    local duration
    local isEnabled = true

    if entry and entry.actionType == "spell" and entry.spellID then
        local spellCooldownInfo = C_Spell.GetSpellCooldown(entry.spellID)
        if spellCooldownInfo then
            startTime = spellCooldownInfo.startTime
            duration = spellCooldownInfo.duration
            isEnabled = spellCooldownInfo.isEnabled
        end
    elseif entry and (entry.actionType == "toy" or entry.actionType == "item") and entry.itemID then
        startTime, duration, isEnabled = C_Item.GetItemCooldown(entry.itemID)
    end

    if startTime and duration and not issecretvalue(startTime) and not issecretvalue(duration) then
        if startTime > 0 and duration > 0 and isEnabled then
            local remaining = max(0, (startTime + duration) - GetTime())

            if remaining > 5 then
                cooldownFrame:SetText(FormatCooldownTime(remaining))
                cooldownOverlay:Show()
                return
            end
        end
    end

    cooldownFrame:SetText("")
    cooldownOverlay:Hide()
end

-- MARK: Build scroll content
function MDTWorldMapTab:BuildScrollContent()
    local scrollFrame = self.panel.ScrollFrame
    local scrollChild = scrollFrame.ScrollChild

    scrollChild.entryPool = scrollChild.entryPool or {}
    scrollChild.headerPool = scrollChild.headerPool or {}

    local function GetHeader(index)
        if not scrollChild.headerPool[index] then
            local header = CreateFrame("Frame", nil, scrollChild)
            header:SetSize(1, 18)
            local text = header:CreateFontString(nil, "OVERLAY", "GameFontNormal")
            text:SetPoint("LEFT", header, "LEFT", 0, 0)
            text:SetFont(GameFontNormal:GetFont(), 12, "OUTLINE")
            text:SetTextColor(1, 1, 1)
            header.text = text
            scrollChild.headerPool[index] = header
        end
        return scrollChild.headerPool[index]
    end

    local function GetEntry(index)
        if not scrollChild.entryPool[index] then
            local entry = CreateFrame("Frame", nil, scrollChild)
            entry:SetSize(1, 32)

            local button = CreateFrame("Button", nil, entry, "SecureActionButtonTemplate")
            button:SetSize(24, 24)
            button:SetPoint("LEFT", entry, "LEFT", 0, 0)
            button:RegisterForClicks("AnyUp", "AnyDown")
            entry.button = button

            local icon = button:CreateTexture(nil, "BACKGROUND")
            icon:SetAllPoints(button)
            icon:SetTexCoord(0.07, 0.93, 0.07, 0.93)
            button.icon = icon

            local cooldownOverlay = button:CreateTexture(nil, "ARTWORK")
            cooldownOverlay:SetAllPoints(button)
            cooldownOverlay:SetTexture("Interface\\Tooltips\\UI-Tooltip-Background")
            cooldownOverlay:SetVertexColor(0, 0, 0, 0.6)
            cooldownOverlay:Hide()
            button.cooldownOverlay = cooldownOverlay

            local borderThickness = 1
            local borderTop = button:CreateTexture(nil, "OVERLAY")
            borderTop:SetColorTexture(0.05, 0.05, 0.05, 1)
            borderTop:SetPoint("TOPLEFT", button, "TOPLEFT", borderThickness, 0)
            borderTop:SetPoint("TOPRIGHT", button, "TOPRIGHT", -borderThickness, 0)
            borderTop:SetHeight(borderThickness)

            local borderBottom = button:CreateTexture(nil, "OVERLAY")
            borderBottom:SetColorTexture(0.05, 0.05, 0.05, 1)
            borderBottom:SetPoint("BOTTOMLEFT", button, "BOTTOMLEFT", borderThickness, 0)
            borderBottom:SetPoint("BOTTOMRIGHT", button, "BOTTOMRIGHT", -borderThickness, 0)
            borderBottom:SetHeight(borderThickness)

            local borderLeft = button:CreateTexture(nil, "OVERLAY")
            borderLeft:SetColorTexture(0.05, 0.05, 0.05, 1)
            borderLeft:SetPoint("TOPLEFT", button, "TOPLEFT", 0, 0)
            borderLeft:SetPoint("BOTTOMLEFT", button, "BOTTOMLEFT", 0, 0)
            borderLeft:SetWidth(borderThickness)

            local borderRight = button:CreateTexture(nil, "OVERLAY")
            borderRight:SetColorTexture(0.05, 0.05, 0.05, 1)
            borderRight:SetPoint("TOPRIGHT", button, "TOPRIGHT", 0, 0)
            borderRight:SetPoint("BOTTOMRIGHT", button, "BOTTOMRIGHT", 0, 0)
            borderRight:SetWidth(borderThickness)

            button.borderTop = borderTop
            button.borderBottom = borderBottom
            button.borderLeft = borderLeft
            button.borderRight = borderRight

            local hover = button:CreateTexture(nil, "HIGHLIGHT")
            hover:SetAllPoints(button)
            hover:SetColorTexture(1, 1, 1, 0.2)
            button.hover = hover

            local cooldownText = button:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
            cooldownText:SetPoint("CENTER", button, "CENTER", 1, 0)
            cooldownText:SetTextColor(1, 1, 1)
            button.cooldownText = cooldownText

            local label = entry:CreateFontString(nil, "OVERLAY", "GameFontNormal")
            label:SetPoint("LEFT", button, "RIGHT", 6, 0)
            label:SetJustifyH("LEFT")
            label:SetJustifyV("TOP")
            label:SetWordWrap(true)
            label:SetFont(GameFontNormal:GetFont(), 11, "OUTLINE")
            label:SetTextColor(1, 1, 1)
            entry.label = label

            local function OnEvent(self, event)
                local currentEntry = self.currentEntry
                if currentEntry and (currentEntry.spellID or currentEntry.itemID) then
                    UpdateCooldownText(self, currentEntry)
                else
                    self.cooldownText:SetText("")
                    self.cooldownOverlay:Hide()
                end
            end

            button:RegisterEvent("SPELLS_CHANGED")
            button:RegisterEvent("PLAYER_ENTERING_WORLD")
            button:RegisterEvent("CHALLENGE_MODE_COMPLETED")
            button:RegisterEvent("BAG_UPDATE_DELAYED")
            button:RegisterEvent("TOYS_UPDATED")
            button:SetScript("OnEvent", OnEvent)

            scrollChild.entryPool[index] = entry
        end
        return scrollChild.entryPool[index]
    end

    local sections = {}
    local function AddSection(title, entries)
        if entries and #entries > 0 then
            table.insert(sections, { title = title, entries = entries })
        end
    end

    local hearthstoneEntries = {}
    local selectedHearthstone = MDungeonTeleports and MDungeonTeleports.selectedHearthstoneId or nil
    for _, entry in ipairs(NormalizeHearthstones(MDTHearthstones, selectedHearthstone, true)) do
        table.insert(hearthstoneEntries, entry)
    end
    for _, entry in ipairs(NormalizeToyList(MDTDalaran, true)) do
        table.insert(hearthstoneEntries, entry)
    end
    for _, entry in ipairs(NormalizeToyList(MDTGarrison, true)) do
        table.insert(hearthstoneEntries, entry)
    end
    AddSection(L["TOOLTIP_WORLD_MAP_TAB_HEARTH"], hearthstoneEntries)

    local _, classTag = UnitClass("player")
    if classTag == "MAGE" and (not MDungeonTeleports or MDungeonTeleports.MagePortals ~= false) then
        local faction = UnitFactionGroup("player")
        local mageTeleports = MDTMage
        local magePortals = nil
        if faction == "Alliance" then
            if MDTMageAllianceTeleports then
                mageTeleports = MDTMageAllianceTeleports
            end
            magePortals = MDTMageAlliancePortals
        elseif faction == "Horde" then
            if MDTMageHordeTeleports then
                mageTeleports = MDTMageHordeTeleports
            end
            magePortals = MDTMageHordePortals
        end

        local mageTeleportEntries = NormalizeSpellList(mageTeleports)
        local magePortalEntries = NormalizeSpellList(magePortals)

        AddSection(L["TOOLTIP_WORLD_MAP_TAB_MAGE"], mageTeleportEntries)
        AddSection(L["TOOLTIP_WORLD_MAP_TAB_MAGE_PORTALS"], magePortalEntries)
    end

    if not MDungeonTeleports or MDungeonTeleports.MapSeason1 ~= false then
        local seasonEntries = {}
        for _, entry in ipairs(NormalizePortalListTableData(MDT_Season1Row1)) do
            table.insert(seasonEntries, entry)
        end
        for _, entry in ipairs(NormalizePortalListTableData(MDT_Season1Row2)) do
            table.insert(seasonEntries, entry)
        end
        AddSection(L["TOOLTIP_WORLD_MAP_TAB_SEASON"], seasonEntries)
    end

    local expansionTables = {
        { name = L["EXP_TWW"], data = MDT_PL_TWW },
        { name = L["EXP_TWW_RAIDS"], data = MDT_PLR_TWW },
        { name = L["EXP_DF"], data = MDT_PL_DF },
        { name = L["EXP_DF_RAIDS"], data = MDT_PLR_DF },
        { name = L["EXP_SL"], data = MDT_PL_SL },
        { name = L["EXP_SL_RAIDS"], data = MDT_PLR_SL },
        { name = L["EXP_BFA"], data = MDT_PL_BFA },
        { name = L["EXP_LEG"], data = MDT_PL_Legion },
        { name = L["EXP_WOD"], data = MDT_PL_WOD },
        { name = L["EXP_MOP"], data = MDT_PL_MOP },
        { name = L["EXP_CATA"], data = MDT_PL_CATA },
        { name = L["EXP_WRATH"], data = MDT_PL_WRATH }
    }

    for _, expansion in ipairs(expansionTables) do
        AddSection(expansion.name or "", NormalizePortalListTableData(expansion.data))
    end

    local function UpdateEntry(entryFrame, entry)
        local button = entryFrame.button
        local label = entryFrame.label

        local displayName = entry.name
        if not entry.useTableData then
            if entry.actionType == "spell" and entry.spellID then
                local spellName = GetSpellName(entry.spellID)
                if spellName and spellName ~= "" then
                    displayName = spellName
                end
            elseif (entry.actionType == "toy" or entry.actionType == "item") and entry.itemID then
                local itemName = GetItemName(entry.itemID)
                if itemName and itemName ~= "" then
                    displayName = itemName
                end
            end
        end
        label:SetText(displayName or "")

        local icon
        if entry.actionType == "spell" then
            if entry.useTableData then
                icon = entry.icon
            else
                icon = GetSpellIcon(entry.spellID)
            end
        elseif entry.preferIcon and entry.icon and entry.icon ~= "" then
            icon = entry.icon
        else
            icon = entry.icon and entry.icon ~= "" and entry.icon or GetItemIcon(entry.itemID)
        end
        button.icon:SetTexture(icon)

        local known = true
        if entry.actionType == "spell" then
            known = IsSpellKnownForDisplay(entry.spellID)
        elseif entry.actionType == "toy" then
            known = IsToyKnown(entry.itemID)
        else
            known = IsItemKnown(entry.itemID)
        end

        button.icon:SetDesaturated(not known)
        if not known then
            label:SetTextColor(0.5, 0.5, 0.5)
        else
            label:SetTextColor(255/255, 221/255, 0/255) -- Text Colour
        end

        if button.cooldownText then
            button.cooldownText:SetFont(MDungeonTeleports.customFont, 14, "OUTLINE")
        end
        button.currentEntry = entry
        if entry and (entry.spellID or entry.itemID) then
            UpdateCooldownText(button, entry)
        else
            if button.cooldownText then
                button.cooldownText:SetText("")
            end
            if button.cooldownOverlay then
                button.cooldownOverlay:Hide()
            end
        end

        if not InCombatLockdown() then
            if entry.actionType == "spell" then
                button:SetAttribute("type", "spell")
                button:SetAttribute("spell", entry.spellID)
            elseif entry.actionType == "toy" then
                button:SetAttribute("type", "toy")
                button:SetAttribute("toy", entry.itemID)
            else
                button:SetAttribute("type", "item")
                button:SetAttribute("item", "item:" .. tostring(entry.itemID))
            end
        end

        button:SetScript("OnEnter", function(self)
            GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
            if entry.actionType == "spell" and entry.spellID then
                GameTooltip:SetSpellByID(entry.spellID)
            elseif entry.actionType == "toy" and entry.itemID then
                GameTooltip:SetToyByItemID(entry.itemID)
            elseif entry.itemID then
                GameTooltip:SetItemByID(entry.itemID)
            end
            GameTooltip:Show()
        end)

        button:SetScript("OnLeave", function()
            GameTooltip:Hide()
        end)
    end

    local function Layout()
        if not scrollFrame or not scrollChild then return end
        local contentWidth = max(1, scrollFrame:GetWidth() - 20)
        local columnGap = 26
        local columnWidth = floor((contentWidth - columnGap) / 2)
        local leftX = 12
        local rightX = leftX + columnWidth + columnGap

        local y = -6
        local headerIndex = 0
        local entryIndex = 0

        for _, section in ipairs(sections) do
            headerIndex = headerIndex + 1
            local header = GetHeader(headerIndex)
            header:Show()
            header:ClearAllPoints()
            header:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", leftX, y)
            header:SetSize(contentWidth, 18)
            header.text:SetText(section.title)
            y = y - 20

            local entries = section.entries or {}
            local rowHeight = 32
            for i, entry in ipairs(entries) do
                entryIndex = entryIndex + 1
                local frame = GetEntry(entryIndex)
                frame:Show()
                frame:ClearAllPoints()

                local col = ((i - 1) % 2) + 1
                local row = floor((i - 1) / 2)
                local x = (col == 1) and leftX or rightX
                local rowY = y - (row * rowHeight)

                frame:SetPoint("TOPLEFT", scrollChild, "TOPLEFT", x, rowY)
                frame:SetSize(columnWidth, rowHeight)
                frame.label:SetWidth(columnWidth - 30)
                UpdateEntry(frame, entry)
            end

            local rows = ceil(#entries / 2)
            y = y - (rows * rowHeight) - 12
        end

        for i = headerIndex + 1, #scrollChild.headerPool do
            scrollChild.headerPool[i]:Hide()
        end
        for i = entryIndex + 1, #scrollChild.entryPool do
            scrollChild.entryPool[i]:Hide()
        end

        scrollChild:SetHeight(max(1, -y + 10))
        scrollChild:SetWidth(contentWidth)
    end

    self.LayoutScrollContent = Layout
    Layout()

    if not scrollFrame.mdtSizeHooked then
        scrollFrame:SetScript("OnSizeChanged", function()
            if MDTWorldMapTab.LayoutScrollContent then
                MDTWorldMapTab:LayoutScrollContent()
            end
        end)
        scrollFrame.mdtSizeHooked = true
    end

    if not self.panel.UpdateFrame then
        local updateFrame = CreateFrame("Frame", nil, self.panel)
        updateFrame:RegisterEvent("SPELLS_CHANGED")
        updateFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
        updateFrame:RegisterEvent("BAG_UPDATE_DELAYED")
        updateFrame:RegisterEvent("TOYS_UPDATED")
        updateFrame:SetScript("OnEvent", function()
            if MDTWorldMapTab.LayoutScrollContent and not InCombatLockdown() then
                MDTWorldMapTab:LayoutScrollContent()
            end
        end)
        self.panel.UpdateFrame = updateFrame
    end
end

-- Toggle the panel visibility
function MDTWorldMapTab:TogglePanel()
    if not self.panel then return end

    if not self:IsEnabled() then
        return
    end

    self:SelectTab()
end

-- Show the panel
function MDTWorldMapTab:ShowPanel()
    if not self.panel then return end

    if not self:IsEnabled() then
        return
    end

    if InCombatLockdown() then
        self.pendingPanelState = "show"
        return
    end

    if WorldMapFrame and WorldMapFrame.SetQuestLogPanelShown then
        local questLogShown = WorldMapFrame.QuestLog and WorldMapFrame.QuestLog:IsShown()
        if not questLogShown then
            self.openedSidePanel = true
            WorldMapFrame:SetQuestLogPanelShown(true)
        else
            self.openedSidePanel = false
        end
    end

    local panelParent = self:GetPanelParent()
    local contentAnchor = self:GetContentAnchor()
    if panelParent and self.panel:GetParent() ~= panelParent then
        self.panel:SetParent(panelParent)
    end
    if contentAnchor then
        self.panel:ClearAllPoints()
        if QuestMapFrame and contentAnchor == QuestMapFrame.ContentsAnchor then
            self.panel:SetPoint("TOPLEFT", contentAnchor, "TOPLEFT", 0, -29)
            self.panel:SetPoint("BOTTOMRIGHT", contentAnchor, "BOTTOMRIGHT", -22, 0)
        else
            self.panel:SetAllPoints(contentAnchor)
        end
        self.panel:SetFrameStrata(contentAnchor:GetFrameStrata())
        self.panel:SetFrameLevel(contentAnchor:GetFrameLevel() + 10)
    end

    self.panel:Show()

    -- Update button state
    if self.button then
        self.button:SetChecked(true)
        if self.button.Icon then
            self.button.Icon:SetTexture(self.button.activeAtlas)
        end
    end
end

-- Hide the panel
function MDTWorldMapTab:HidePanel()
    if not self.panel then return end

    if InCombatLockdown() then
        self.pendingPanelState = "hide"
        return
    end

    self.panel:Hide()

    if self.openedSidePanel and WorldMapFrame and WorldMapFrame.SetQuestLogPanelShown then
        WorldMapFrame:SetQuestLogPanelShown(false)
        self.openedSidePanel = false
    end

    if self.button then
        self.button:SetChecked(false)
        if self.button.Icon then
            self.button.Icon:SetTexture(self.button.inactiveAtlas)
        end
    end
end

function MDTWorldMapTab:UpdateVisibility(enabled)
    if enabled then
        if not self.initialized then
            self:Initialize()
            return
        end
        if self.button then
            self.button:Show()
            self:AnchorButton()
        end
        return
    end

    if self.panel then
        self:HidePanel()
    end
    if self.button then
        self.button:Hide()
    end
end

-- MARK: Event handling
local eventFrame = CreateFrame("Frame")
eventFrame:RegisterEvent("ADDON_LOADED")
eventFrame:RegisterEvent("PLAYER_LOGIN")
eventFrame:RegisterEvent("PLAYER_ENTERING_WORLD")
eventFrame:RegisterEvent("PLAYER_REGEN_ENABLED")
eventFrame:SetScript("OnEvent", function(self, event, ...)
    if event == "ADDON_LOADED" then
        local addonName = ...
        if addonName == "MDungeonTeleports" then
            -- Addon loaded
        elseif addonName == "ElvUI" then
            if MDTWorldMapTab.button then
                MDTWorldMapTab:ApplyElvUISkin()
            end
        end
    elseif event == "PLAYER_LOGIN" then
        -- Initialize after login
        C_Timer.After(1, function()
            if not MDTWorldMapTab.initialized and MDTWorldMapTab:IsEnabled() then
                MDTWorldMapTab:Initialize()
            end
        end)
    elseif event == "PLAYER_ENTERING_WORLD" then
        -- Also try on entering world
        C_Timer.After(2, function()
            if not MDTWorldMapTab.initialized and MDTWorldMapTab:IsEnabled() then
                MDTWorldMapTab:Initialize()
            end
        end)
    elseif event == "PLAYER_REGEN_ENABLED" then
        if MDTWorldMapTab.pendingPanelState == "show" then
            MDTWorldMapTab.pendingPanelState = nil
            MDTWorldMapTab:ShowPanel()
        elseif MDTWorldMapTab.pendingPanelState == "hide" then
            MDTWorldMapTab.pendingPanelState = nil
            MDTWorldMapTab:HidePanel()
        end
        if MDTWorldMapTab.pendingContentRefresh then
            MDTWorldMapTab.pendingContentRefresh = nil
            if MDTWorldMapTab.panel then
                MDTWorldMapTab:BuildScrollContent()
            end
        end
    end
end)

-- MARK: Handle WorldMap show/hide with delayed hook
C_Timer.After(3, function()
    if WorldMapFrame then
        hooksecurefunc(WorldMapFrame, "Show", function()
            if MDTWorldMapTab.initialized and MDTWorldMapTab.button and MDTWorldMapTab:IsEnabled() then
                MDTWorldMapTab.button:Show()
                MDTWorldMapTab:AnchorButton()
                MDTWorldMapTab:HookOtherTabs()
                MDTWorldMapTab:HookOtherContentFrames()
                MDTWorldMapTab:HookDisplayMode()
                if QuestMapFrame and QuestMapFrame.displayMode == DISPLAY_MODE then
                    MDTWorldMapTab:ShowPanel()
                end
            elseif not MDTWorldMapTab.initialized and MDTWorldMapTab:IsEnabled() then
                -- Try to initialize if not done yet
                MDTWorldMapTab:Initialize()
            end
        end)
        
        hooksecurefunc(WorldMapFrame, "Hide", function()
            if MDTWorldMapTab.initialized and MDTWorldMapTab.panel then
                MDTWorldMapTab:HidePanel()
            end
        end)
    end
end)

NS.WorldMapTab = MDTWorldMapTab