local _, Addon = ...

local Markers = {}
Addon.Markers = Markers
local L = Addon.L

local state = Addon.state
local Coordinates = Addon.Coordinates
local canvas
local viewport
local mapFrame
local statusText
local playerMarker
local waypointMarker
local corpseMarker
local nativePlayerFrame
local nativeMapID
local groupMarkers = {}
local contentMarkers = {}
local bossMarkers = {}
local enemyMarkers = {}
local currentGossipPoiID
local currentGossipX
local currentGossipY
local dismissedGossipPoiID

local GOSSIP_ARRIVAL_DISTANCE = 25

local function PositionMarker(marker, x, y)
    Addon.MapView:PositionMarker(marker, x, y)
end

local function CreatePrimaryMarkers()
    playerMarker = CreateFrame("Frame", nil, canvas)
    playerMarker:SetSize(Addon.db.playerArrowSize, Addon.db.playerArrowSize)
    playerMarker:SetFrameLevel(canvas:GetFrameLevel() + 20)

    local arrow = playerMarker:CreateTexture(nil, "OVERLAY")
    arrow:SetAllPoints()
    arrow:SetTexture("Interface\\Minimap\\MinimapArrow")
    playerMarker.arrow = arrow

    waypointMarker = CreateFrame("Frame", nil, canvas)
    waypointMarker:SetSize(24, 24)
    waypointMarker:SetFrameLevel(canvas:GetFrameLevel() + 19)

    local waypointTexture = waypointMarker:CreateTexture(nil, "OVERLAY")
    waypointTexture:SetAllPoints()
    waypointTexture:SetAtlas("Waypoint-MapPin-Untracked", false)

    corpseMarker = CreateFrame("Frame", nil, canvas)
    corpseMarker:SetSize(24, 24)
    corpseMarker:SetFrameLevel(canvas:GetFrameLevel() + 22)
    corpseMarker:EnableMouse(true)

    local corpseTexture = corpseMarker:CreateTexture(nil, "ARTWORK")
    corpseTexture:SetAllPoints()
    corpseTexture:SetTexture("Interface\\Minimap\\POIIcons")
    corpseTexture:SetTexCoord(0.56640625, 0.6328125, 0.001953125, 0.03515625)
    corpseMarker:SetScript("OnEnter", function(marker)
        GameTooltip:SetOwner(marker, "ANCHOR_RIGHT")
        GameTooltip:SetText(CORPSE_RED or L.CORPSE)
        GameTooltip:Show()
    end)
    corpseMarker:SetScript("OnLeave", GameTooltip_Hide)
    corpseMarker:Hide()

    -- El mapa oficial usa este widget nativo para las posiciones protegidas.
    -- Puede dibujar al jugador en instancias aunque las APIs de coordenadas
    -- devuelvan nil al código Lua del addon.
    local ok, frame = pcall(
        CreateFrame,
        "UnitPositionFrame",
        nil,
        canvas,
        "UnitPositionFrameTemplate"
    )
    if ok and frame then
        nativePlayerFrame = frame
        nativePlayerFrame:SetAllPoints(canvas)
        nativePlayerFrame:SetFrameLevel(canvas:GetFrameLevel() + 21)
        nativePlayerFrame:EnableMouse(false)

        local configured = pcall(function()
            nativePlayerFrame:SetPinSize("player", Addon.db.playerArrowSize)
            nativePlayerFrame:SetPinTexture("player", "UI-WorldMapArrow")
            nativePlayerFrame:SetUseClassColor("player", false)
            nativePlayerFrame:SetPinSize("party", 11)
            nativePlayerFrame:SetPinSize("raid", 9)
            nativePlayerFrame:SetUseClassColor("party", true)
            nativePlayerFrame:SetUseClassColor("raid", true)
            nativePlayerFrame:SetShouldShowUnits("player", true)
            nativePlayerFrame:SetShouldShowUnits("party", true)
            nativePlayerFrame:SetShouldShowUnits("raid", true)
            nativePlayerFrame:SetMouseOverUnitExcluded("player", true)
            nativePlayerFrame:SetScript("OnUpdate", function(self)
                self:UpdatePlayerPins()
            end)
        end)
        if not configured then
            nativePlayerFrame:Hide()
            nativePlayerFrame = nil
        else
            nativePlayerFrame:Hide()
        end
    end
end

local function AcquireGroupMarker(index)
    if not groupMarkers[index] then
        local marker = CreateFrame("Frame", nil, canvas, "BackdropTemplate")
        marker:SetSize(10, 10)
        marker:SetFrameLevel(canvas:GetFrameLevel() + 18)
        marker:SetBackdrop({
            bgFile = "Interface\\Buttons\\WHITE8X8",
            edgeFile = "Interface\\Buttons\\WHITE8X8",
            edgeSize = 1,
        })
        marker:SetBackdropBorderColor(0, 0, 0, 1)
        groupMarkers[index] = marker
    end

    return groupMarkers[index]
end

local function HideGroupMarkers(firstUnused)
    for index = firstUnused, #groupMarkers do
        groupMarkers[index]:Hide()
    end
end

local function RestoreShapeVisibility(marker)
    if not marker or not marker._mzmShapeHidden then
        return
    end
    marker:SetAlpha(marker._mzmShapeAlpha or 1)
    if marker.EnableMouse and marker._mzmShapeMouseEnabled ~= nil then
        marker:EnableMouse(marker._mzmShapeMouseEnabled)
    end
    marker._mzmShapeHidden = nil
    marker._mzmShapeAlpha = nil
    marker._mzmShapeMouseEnabled = nil
end

local function SetShapeHidden(marker)
    if not marker._mzmShapeHidden then
        marker._mzmShapeHidden = true
        marker._mzmShapeAlpha = marker:GetAlpha()
        if marker.IsMouseEnabled then
            marker._mzmShapeMouseEnabled = marker:IsMouseEnabled()
        end
    end
    marker:SetAlpha(0)
    if marker.EnableMouse then
        marker:EnableMouse(false)
    end
end

local function ApplyShapeVisibility(marker)
    if not marker then
        return
    end
    if Addon.db.shape ~= "circle" then
        RestoreShapeVisibility(marker)
        return
    end
    if not marker:IsShown() then
        return
    end

    local markerX, markerY = marker:GetCenter()
    local viewX, viewY = viewport:GetCenter()
    if Addon:IsSecret(markerX)
        or Addon:IsSecret(markerY)
        or Addon:IsSecret(viewX)
        or Addon:IsSecret(viewY)
    then
        return
    end
    if not markerX or not markerY or not viewX or not viewY then
        return
    end

    local markerRadius = math.max(marker:GetWidth() or 0, marker:GetHeight() or 0) * 0.35
    local radius = (math.min(viewport:GetWidth(), viewport:GetHeight()) / 2) - markerRadius
    local deltaX = markerX - viewX
    local deltaY = markerY - viewY
    if (deltaX * deltaX) + (deltaY * deltaY) <= radius * radius then
        RestoreShapeVisibility(marker)
    else
        SetShapeHidden(marker)
    end
end

local function ApplyShapeToChildren(frame, depth)
    if not frame or depth <= 0 then
        return
    end
    for _, child in ipairs({ frame:GetChildren() }) do
        if math.max(child:GetWidth() or 0, child:GetHeight() or 0) <= 80 then
            ApplyShapeVisibility(child)
        else
            RestoreShapeVisibility(child)
        end
        ApplyShapeToChildren(child, depth - 1)
    end
end

local function SetMarkerAtlas(texture, atlasName, textureIndex, textureKit)
    texture:SetTexCoord(0, 1, 0, 1)

    if type(atlasName) == "string" and atlasName ~= "" then
        local resolvedAtlas = atlasName
        if type(textureKit) == "string" and textureKit ~= "" then
            local textureKitAtlas = string.format("%s-%s", textureKit, atlasName)
            local kitOK, kitAtlasInfo = pcall(C_Texture.GetAtlasInfo, textureKitAtlas)
            if kitOK and kitAtlasInfo then
                resolvedAtlas = textureKitAtlas
            end
        end

        local ok, atlasInfo = pcall(C_Texture.GetAtlasInfo, resolvedAtlas)
        if ok and atlasInfo then
            texture:SetAtlas(resolvedAtlas, false)
            return true
        end
    end

    if type(textureIndex) == "number" and C_Minimap.GetPOITextureCoords then
        local ok, x1, x2, y1, y2 = pcall(C_Minimap.GetPOITextureCoords, textureIndex)
        if ok and x1 and x2 and y1 and y2 then
            texture:SetTexture("Interface\\Minimap\\POIIcons")
            texture:SetTexCoord(x1, x2, y1, y2)
            return true
        end
    end

    texture:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark")
    texture:SetTexCoord(0.08, 0.92, 0.08, 0.92)
    return false
end

function Markers:GetGossipPOI(mapID)
    if not mapID
        or not C_GossipInfo
        or not C_GossipInfo.GetPoiForUiMapID
        or not C_GossipInfo.GetPoiInfo
    then
        return nil
    end

    local poiOK, gossipPoiID = pcall(C_GossipInfo.GetPoiForUiMapID, mapID)
    if not poiOK or not gossipPoiID or Addon:IsSecret(gossipPoiID) then
        currentGossipPoiID = nil
        currentGossipX = nil
        currentGossipY = nil
        dismissedGossipPoiID = nil
        return nil
    end

    if gossipPoiID ~= currentGossipPoiID then
        currentGossipPoiID = gossipPoiID
        dismissedGossipPoiID = nil
    elseif gossipPoiID == dismissedGossipPoiID then
        return nil
    end

    local infoOK, info = pcall(C_GossipInfo.GetPoiInfo, mapID, gossipPoiID)
    if not infoOK or not info then
        return nil
    end

    local x, y = Coordinates:SafeGetXY(info.position)
    if not x or not y then
        return nil
    end
    currentGossipX = x
    currentGossipY = y
    return gossipPoiID, info, x, y
end

function Markers:ResetGossipPOIState()
    currentGossipPoiID = nil
    currentGossipX = nil
    currentGossipY = nil
    dismissedGossipPoiID = nil
end

function Markers:UpdateGossipArrival(playerX, playerY)
    if not currentGossipPoiID
        or dismissedGossipPoiID == currentGossipPoiID
        or not currentGossipX
        or not currentGossipY
        or not playerX
        or not playerY
        or not state.mapID
    then
        return false
    end

    local sizeOK, worldWidth, worldHeight = pcall(C_Map.GetMapWorldSize, state.mapID)
    if not sizeOK
        or Addon:IsSecret(worldWidth)
        or Addon:IsSecret(worldHeight)
        or type(worldWidth) ~= "number"
        or type(worldHeight) ~= "number"
        or worldWidth <= 0
        or worldHeight <= 0
    then
        return false
    end

    local deltaX = (currentGossipX - playerX) * worldWidth
    local deltaY = (currentGossipY - playerY) * worldHeight
    if ((deltaX * deltaX) + (deltaY * deltaY)) ^ 0.5 > GOSSIP_ARRIVAL_DISTANCE then
        return false
    end

    dismissedGossipPoiID = currentGossipPoiID
    for _, marker in ipairs(contentMarkers) do
        if marker.gossipPoiID == dismissedGossipPoiID then
            marker:Hide()
        end
    end
    return true
end

local function GetQuestTitle(questID)
    local title = C_QuestLog.GetTitleForQuestID(questID)
    if type(title) == "string" and title ~= "" and not Addon:IsSecret(title) then
        return title
    end

    if C_TaskQuest.GetQuestInfoByQuestID then
        local taskTitle = C_TaskQuest.GetQuestInfoByQuestID(questID)
        if type(taskTitle) == "string" and taskTitle ~= "" and not Addon:IsSecret(taskTitle) then
            return taskTitle
        end
    end

    return Addon:Format("QUEST_FALLBACK", questID)
end

local function GetQuestClassification(questID)
    if not C_QuestInfoSystem or not C_QuestInfoSystem.GetQuestClassification then
        return nil
    end

    local ok, classification = pcall(C_QuestInfoSystem.GetQuestClassification, questID)
    if ok then
        return classification
    end
    return nil
end

local function IsRecurringQuestFrequency(frequency)
    if not Enum or not Enum.QuestFrequency or frequency == nil or Addon:IsSecret(frequency) then
        return false
    end

    return frequency == Enum.QuestFrequency.Daily
        or frequency == Enum.QuestFrequency.Weekly
        or frequency == Enum.QuestFrequency.ResetByScheduler
end

local function GetQuestFrequency(questID, info)
    if info and IsRecurringQuestFrequency(info.frequency) then
        return info.frequency
    end

    if not C_QuestLog or not C_QuestLog.GetLogIndexForQuestID or not C_QuestLog.GetInfo then
        return nil
    end

    local indexOK, questLogIndex = pcall(C_QuestLog.GetLogIndexForQuestID, questID)
    if not indexOK or not questLogIndex or Addon:IsSecret(questLogIndex) then
        return nil
    end

    local infoOK, questInfo = pcall(C_QuestLog.GetInfo, questLogIndex)
    if infoOK and questInfo and not Addon:IsSecret(questInfo) then
        return questInfo.frequency
    end
    return nil
end

local function BlizzardUsesRecurringQuestIcon(questID)
    if not QuestUtil or not QuestUtil.GetQuestIconOfferForQuestID then
        return false
    end

    local ok, asset = pcall(QuestUtil.GetQuestIconOfferForQuestID, questID)
    if not ok or type(asset) ~= "string" or Addon:IsSecret(asset) then
        return false
    end

    asset = asset:lower()
    return asset:find("recurring", 1, true) ~= nil
        or asset:find("dailyactivequesticon", 1, true) ~= nil
end

local function IsRecurringQuest(questID, info)
    if info and (info.isDaily or info.isWeekly) then
        return true
    end

    if IsRecurringQuestFrequency(GetQuestFrequency(questID, info)) then
        return true
    end

    -- QuestUtil reads Blizzard's internal QuestCache, whose frequency remains
    -- available for some weekly item quests that report a normal classification.
    if BlizzardUsesRecurringQuestIcon(questID) then
        return true
    end

    if C_QuestLog and C_QuestLog.IsRepeatableQuest then
        local ok, isRepeatable = pcall(C_QuestLog.IsRepeatableQuest, questID)
        if ok and not Addon:IsSecret(isRepeatable) and isRepeatable then
            return true
        end
    end

    return false
end

local function IsQuestClassification(classification, name)
    return Enum
        and Enum.QuestClassification
        and classification == Enum.QuestClassification[name]
end

local function GetQuestVisualClassification(questID, info)
    local classification = GetQuestClassification(questID)
    local isNormal = classification == nil
        or IsQuestClassification(classification, "Normal")
        or IsQuestClassification(classification, "Questline")
    if not isNormal then
        return classification
    end

    if info then
        if info.isCampaign then
            return Enum.QuestClassification.Campaign
        elseif info.isLegendary then
            return Enum.QuestClassification.Legendary
        elseif info.isImportant then
            return Enum.QuestClassification.Important
        elseif info.isMeta then
            return Enum.QuestClassification.Meta
        end
    end

    if IsRecurringQuest(questID, info) then
        return Enum.QuestClassification.Recurring
    end

    return classification
end

local function GetQuestRingAtlas(classification, superTracked)
    local suffix = superTracked and "-SuperTracked" or ""
    if IsQuestClassification(classification, "Legendary") then
        return "UI-QuestPoiLegendary-QuestNumber" .. suffix
    elseif IsQuestClassification(classification, "Campaign")
        or IsQuestClassification(classification, "Calling") then
        return "UI-QuestPoiCampaign-QuestNumber" .. suffix
    elseif IsQuestClassification(classification, "Recurring") then
        return "UI-QuestPoiRecurring-QuestNumber" .. suffix
    elseif IsQuestClassification(classification, "Important") then
        return "UI-QuestPoiImportant-QuestNumber" .. suffix
    elseif IsQuestClassification(classification, "Meta") then
        return "UI-QuestPoiWrapper-QuestNumber" .. suffix
    end

    return "UI-QuestPoi-QuestNumber" .. suffix
end

local function GetQuestGlowAtlas(classification)
    if IsQuestClassification(classification, "Legendary") then
        return "UI-QuestPoiLegendary-OuterGlow"
    elseif IsQuestClassification(classification, "Campaign")
        or IsQuestClassification(classification, "Calling") then
        return "UI-QuestPoiCampaign-OuterGlow"
    elseif IsQuestClassification(classification, "Recurring") then
        return "UI-QuestPoiRecurring-OuterGlow"
    elseif IsQuestClassification(classification, "Important") then
        return "UI-QuestPoiImportant-OuterGlow"
    elseif IsQuestClassification(classification, "Meta") then
        return "UI-QuestPoiWrapper-OuterGlow"
    end

    return "UI-QuestPoi-OuterGlow"
end

local function GetQuestTurnInAtlas(classification)
    if IsQuestClassification(classification, "Legendary") then
        return "UI-QuestPoiLegendary-QuestBangTurnIn"
    elseif IsQuestClassification(classification, "Campaign") then
        return "UI-QuestPoiCampaign-QuestBangTurnIn"
    elseif IsQuestClassification(classification, "Calling") then
        return "UI-DailyQuestPoiCampaign-QuestBangTurnIn"
    elseif IsQuestClassification(classification, "Recurring") then
        return "UI-QuestPoiRecurring-QuestBangTurnIn"
    elseif IsQuestClassification(classification, "Important") then
        return "UI-QuestPoiImportant-QuestBangTurnIn"
    elseif IsQuestClassification(classification, "Meta") then
        return "UI-QuestPoiWrapper-QuestBangTurnIn"
    end

    return "UI-QuestIcon-TurnIn-Normal"
end

local function GetStandaloneActiveQuestAtlas(classification, isComplete)
    if IsQuestClassification(classification, "Campaign") then
        return isComplete and "CampaignActiveQuestIcon" or "CampaignInProgressQuestIcon"
    elseif IsQuestClassification(classification, "Calling") then
        return isComplete and "CampaignActiveDailyQuestIcon" or "CampaignInProgressQuestIcon"
    elseif IsQuestClassification(classification, "Recurring") then
        return isComplete and "Recurringactivequesticon" or "RepeatableInProgressquesticon"
    elseif IsQuestClassification(classification, "Legendary") then
        return isComplete and "legendaryactivequesticon" or "legendaryInProgressquesticon"
    elseif IsQuestClassification(classification, "Important") then
        return isComplete and "importantactivequesticon" or "importantInProgressquesticon"
    elseif IsQuestClassification(classification, "Meta") then
        return isComplete and "Wrapperactivequesticon" or "WrapperInProgressquesticon"
    end
    return nil
end

local function GetActiveQuestVisual(questID)
    local classification = GetQuestVisualClassification(questID)
    local superTrackedQuestID = C_SuperTrack
        and C_SuperTrack.GetSuperTrackedQuestID
        and C_SuperTrack.GetSuperTrackedQuestID()
    local superTracked = superTrackedQuestID
        and not Addon:IsSecret(superTrackedQuestID)
        and superTrackedQuestID == questID
    local isComplete = C_QuestLog.IsComplete(questID)
    local standaloneAtlas = GetStandaloneActiveQuestAtlas(classification, isComplete)
    if standaloneAtlas then
        return standaloneAtlas, classification, nil, GetQuestGlowAtlas(classification), true
    end

    local iconAtlas
    if isComplete then
        iconAtlas = GetQuestTurnInAtlas(classification)
    else
        iconAtlas = superTracked and "Quest-In-Progress-Icon-Brown" or "Quest-In-Progress-Icon-yellow"
    end

    return iconAtlas,
        classification,
        GetQuestRingAtlas(classification, superTracked),
        GetQuestGlowAtlas(classification)
end

local function GetAvailableQuestVisual(info)
    local classification = GetQuestVisualClassification(info.questID, info)
    local atlasName
    if IsQuestClassification(classification, "Recurring") then
        atlasName = "Recurringavailablequesticon"
    elseif IsQuestClassification(classification, "Meta") then
        atlasName = "quest-wrapper-available"
    elseif IsQuestClassification(classification, "Calling") then
        atlasName = "Quest-DailyCampaign-Available"
    elseif IsQuestClassification(classification, "Campaign") then
        atlasName = "Quest-Campaign-Available"
    elseif IsQuestClassification(classification, "Legendary") then
        atlasName = "UI-QuestPoiLegendary-QuestBang"
    elseif IsQuestClassification(classification, "Important") then
        atlasName = "importantavailablequesticon"
    end

    return atlasName or "QuestNormal", classification, GetQuestGlowAtlas(classification)
end

local function GetAvailableQuestLabel(classification, info)
    if IsQuestClassification(classification, "Recurring") then
        return L.RECURRING_QUEST_AVAILABLE
    elseif IsQuestClassification(classification, "Campaign")
        or IsQuestClassification(classification, "Calling")
        or info.isCampaign then
        return L.CAMPAIGN_QUEST_AVAILABLE
    end

    return L.QUEST_AVAILABLE
end

local function GetWorldQuestAtlas(questID, inProgress)
    local tagInfo = C_QuestLog.GetQuestTagInfo(questID)
    if QuestUtil and QuestUtil.GetWorldQuestAtlasInfo and tagInfo then
        local ok, atlasName = pcall(QuestUtil.GetWorldQuestAtlasInfo, questID, tagInfo, inProgress)
        if ok and atlasName then
            return atlasName
        end
    end

    return "Worldquest-icon"
end

local function ShowContentMarkerTooltip(marker)
    GameTooltip:SetOwner(marker, "ANCHOR_LEFT")
    GameTooltip:SetText(marker.title or L.MAP_POINT, 1, 0.82, 0)

    if marker.kindLabel then
        GameTooltip:AddLine(marker.kindLabel, 0.45, 0.8, 1)
    end
    if marker.description and marker.description ~= "" then
        GameTooltip:AddLine(marker.description, 1, 1, 1, true)
    end
    if marker.questID then
        local tagInfo = C_QuestLog.GetQuestTagInfo(marker.questID)
        if tagInfo and type(tagInfo.tagName) == "string" and not Addon:IsSecret(tagInfo.tagName) then
            GameTooltip:AddLine(tagInfo.tagName, 0.75, 0.75, 0.75)
        end

        local objectives = C_QuestLog.GetQuestObjectives(marker.questID)
        if objectives then
            for index, objective in ipairs(objectives) do
                if index > 6 then
                    break
                end
                if type(objective.text) == "string" and not Addon:IsSecret(objective.text) then
                    local color = objective.finished and GREEN_FONT_COLOR or HIGHLIGHT_FONT_COLOR
                    GameTooltip:AddLine(objective.text, color.r, color.g, color.b, true)
                end
            end
        end

        if C_QuestLog.IsWorldQuest(marker.questID) and C_TaskQuest.GetQuestTimeLeftMinutes then
            local minutes = C_TaskQuest.GetQuestTimeLeftMinutes(marker.questID)
            if type(minutes) == "number" and minutes > 0 and not Addon:IsSecret(minutes) then
                local hours = math.floor(minutes / 60)
                local remainingMinutes = math.floor(minutes % 60)
                if hours > 0 then
                    GameTooltip:AddLine(Addon:Format("TIME_REMAINING_HOURS", hours, remainingMinutes), 1, 0.55, 0.2)
                else
                    GameTooltip:AddLine(Addon:Format("TIME_REMAINING_MINUTES", remainingMinutes), 1, 0.55, 0.2)
                end
            end
        end
    end

    if marker.trackable or marker.areaPoiID then
        GameTooltip:AddLine(L.LEFT_CLICK_TRACK, 0.6, 0.8, 1)
    end
    if marker.linkedUiMapID then
        GameTooltip:AddLine(L.RIGHT_CLICK_OPEN_WORLD_MAP, 0.6, 0.8, 1)
    end
    GameTooltip:Show()
end

local function TrackContentMarker(marker, button)
    if button == "RightButton" and marker.linkedUiMapID then
        pcall(C_Map.OpenWorldMap, marker.linkedUiMapID)
        return
    end

    if marker.questID and marker.trackable then
        pcall(C_SuperTrack.SetSuperTrackedQuestID, marker.questID)
    elseif marker.superTrackType and marker.superTrackID then
        pcall(C_SuperTrack.SetSuperTrackedMapPin, marker.superTrackType, marker.superTrackID)
    elseif marker.areaPoiID and Enum.SuperTrackingMapPinType.AreaPOI then
        pcall(C_SuperTrack.SetSuperTrackedMapPin, Enum.SuperTrackingMapPinType.AreaPOI, marker.areaPoiID)
    end
end

local function ShowBossMarkerTooltip(marker)
    GameTooltip:SetOwner(marker, "ANCHOR_LEFT")
    GameTooltip:SetText(marker.title or L.DUNGEON_BOSS, 1, 0.82, 0)
    if marker.completed then
        GameTooltip:AddLine(DUNGEON_ENCOUNTER_DEFEATED or L.DEFEATED, 1, 0.2, 0.2)
    else
        GameTooltip:AddLine(L.DUNGEON_OR_RAID_BOSS, 0.45, 0.8, 1)
    end
    if marker.description and marker.description ~= "" then
        GameTooltip:AddLine(marker.description, 1, 1, 1, true)
    end
    if marker.routeAssignment then
        local red, green, blue = Addon.Routes:GetColor(marker.routeAssignment.color)
        GameTooltip:AddLine(Addon:Format("PULL_FORMAT", marker.routeAssignment.pull), red, green, blue)
    end
    GameTooltip:AddLine(L.CLICK_OPEN_ADVENTURE_GUIDE, 0.6, 0.8, 1)
    GameTooltip:Show()
end

local function OpenBossJournal(marker)
    pcall(function()
        EncounterJournal_LoadUI()
        EncounterJournal_OpenJournal(nil, marker.instanceID, marker.encounterID)
    end)
end

local function AcquireBossMarker(index)
    if not bossMarkers[index] then
        local marker = CreateFrame("Button", nil, canvas)
        marker:SetSize(32, 32)
        marker:RegisterForClicks("LeftButtonUp")

        local portrait = marker:CreateTexture(nil, "BACKGROUND")
        portrait:SetPoint("CENTER")
        portrait:SetSize(23, 23)
        marker.portrait = portrait

        local portraitMask = marker:CreateMaskTexture(nil, "ARTWORK")
        portraitMask:SetTexture("Interface\\CharacterFrame\\TempPortraitAlphaMask")
        portraitMask:SetAllPoints(portrait)
        portrait:AddMaskTexture(portraitMask)

        local border = marker:CreateTexture(nil, "ARTWORK")
        border:SetAllPoints()
        border:SetTexture("Interface\\EncounterJournal\\UI-EncounterJournalTextures")
        border:SetTexCoord(0.84960938, 0.97070313, 0.42871094, 0.48828125)

        local highlight = marker:CreateTexture(nil, "HIGHLIGHT")
        highlight:SetAllPoints()
        highlight:SetTexture("Interface\\EncounterJournal\\UI-EncounterJournalTextures")
        highlight:SetTexCoord(0.68945313, 0.81054688, 0.33300781, 0.39257813)
        marker:SetHighlightTexture(highlight)

        local defeatedCross = marker:CreateTexture(nil, "OVERLAY")
        defeatedCross:SetPoint("BOTTOMRIGHT", 2, -2)
        defeatedCross:SetAtlas("Map-MarkedDefeated", true)
        defeatedCross:SetScale(0.65)
        marker.defeatedCross = defeatedCross

        local pullBadge = marker:CreateTexture(nil, "OVERLAY", nil, 2)
        pullBadge:SetPoint("TOPLEFT", 0, 0)
        pullBadge:SetSize(16, 16)
        pullBadge:SetTexture("Interface\\Buttons\\WHITE8X8")
        marker.pullBadge = pullBadge

        local pullMask = marker:CreateMaskTexture(nil, "OVERLAY")
        pullMask:SetTexture("Interface\\CharacterFrame\\TempPortraitAlphaMask")
        pullMask:SetAllPoints(pullBadge)
        pullBadge:AddMaskTexture(pullMask)

        local pullText = marker:CreateFontString(nil, "OVERLAY", "GameFontNormalTiny")
        pullText:SetPoint("CENTER", pullBadge, "CENTER", 0, 0)
        marker.pullText = pullText

        marker:SetScript("OnEnter", ShowBossMarkerTooltip)
        marker:SetScript("OnLeave", GameTooltip_Hide)
        marker:SetScript("OnClick", OpenBossJournal)
        bossMarkers[index] = marker
    end

    return bossMarkers[index]
end

local function HideBossMarkers(firstUnused)
    for index = firstUnused, #bossMarkers do
        bossMarkers[index]:Hide()
    end
end

local function ShowEnemyMarkerTooltip(marker)
    local enemy = marker.enemy
    if not enemy then
        return
    end

    GameTooltip:SetOwner(marker, "ANCHOR_LEFT")
    GameTooltip:SetText(enemy.name or L.DUNGEON_ENEMY, 1, 0.82, 0)
    if enemy.level and enemy.level > 0 then
        local detail = Addon:Format("LEVEL_FORMAT", enemy.level)
        if enemy.creatureType and enemy.creatureType ~= "" then
            detail = detail .. "  |  " .. enemy.creatureType
        end
        GameTooltip:AddLine(detail, 0.75, 0.75, 0.75)
    end
    if enemy.forces and enemy.forces > 0 then
        GameTooltip:AddLine(Addon:Format("ENEMY_FORCES_FORMAT", enemy.forces), 0.35, 0.85, 1)
    end
    if marker.routeAssignment then
        local red, green, blue = Addon.Routes:GetColor(marker.routeAssignment.color)
        GameTooltip:AddLine(Addon:Format("PULL_FORMAT", marker.routeAssignment.pull), red, green, blue)
    end
    if marker.killed then
        GameTooltip:AddLine(L.KILLED, 0.6, 0.65, 0.7)
    end
    if enemy.interruptible then
        GameTooltip:AddLine(L.INTERRUPTIBLE_SPELLS, 1, 0.55, 0.2)
    end
    if enemy.patrol then
        GameTooltip:AddLine(L.PATROL, 0.45, 0.75, 1)
    end
    if enemy.npcID and enemy.npcID > 0 then
        GameTooltip:AddLine(Addon:Format("NPC_FORMAT", enemy.npcID), 0.5, 0.5, 0.5)
    end
    GameTooltip:Show()
end

local function AcquireEnemyMarker(index)
    if not enemyMarkers[index] then
        local marker = CreateFrame("Button", nil, canvas)
        marker:SetSize(18, 18)

        local ring = marker:CreateTexture(nil, "ARTWORK")
        ring:SetAllPoints()
        ring:SetTexture("Interface\\Buttons\\WHITE8X8")
        marker.ring = ring

        local outerMask = marker:CreateMaskTexture(nil, "ARTWORK")
        outerMask:SetTexture("Interface\\CharacterFrame\\TempPortraitAlphaMask")
        outerMask:SetAllPoints(ring)
        ring:AddMaskTexture(outerMask)

        local portrait = marker:CreateTexture(nil, "ARTWORK", nil, 1)
        portrait:SetPoint("TOPLEFT", 2, -2)
        portrait:SetPoint("BOTTOMRIGHT", -2, 2)
        portrait:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark")
        marker.portrait = portrait

        local innerMask = marker:CreateMaskTexture(nil, "ARTWORK")
        innerMask:SetTexture("Interface\\CharacterFrame\\TempPortraitAlphaMask")
        innerMask:SetAllPoints(portrait)
        portrait:AddMaskTexture(innerMask)

        local badge = marker:CreateTexture(nil, "OVERLAY", nil, 2)
        badge:SetPoint("BOTTOMRIGHT", 2, -2)
        badge:SetSize(11, 11)
        badge:SetTexture("Interface\\Buttons\\WHITE8X8")
        badge:SetVertexColor(0.015, 0.02, 0.025, 0.98)
        marker.badge = badge

        local badgeMask = marker:CreateMaskTexture(nil, "OVERLAY")
        badgeMask:SetTexture("Interface\\CharacterFrame\\TempPortraitAlphaMask")
        badgeMask:SetAllPoints(badge)
        badge:AddMaskTexture(badgeMask)

        local forceText = marker:CreateFontString(nil, "OVERLAY", "GameFontNormalTiny")
        forceText:SetPoint("CENTER", badge, "CENTER", 0, 0)
        forceText:SetTextColor(1, 1, 1)
        marker.forceText = forceText

        local pullBadge = marker:CreateTexture(nil, "OVERLAY", nil, 3)
        pullBadge:SetPoint("TOPLEFT", -3, 3)
        pullBadge:SetSize(14, 14)
        pullBadge:SetTexture("Interface\\Buttons\\WHITE8X8")
        marker.pullBadge = pullBadge

        local pullMask = marker:CreateMaskTexture(nil, "OVERLAY")
        pullMask:SetTexture("Interface\\CharacterFrame\\TempPortraitAlphaMask")
        pullMask:SetAllPoints(pullBadge)
        pullBadge:AddMaskTexture(pullMask)

        local pullText = marker:CreateFontString(nil, "OVERLAY", "GameFontNormalTiny")
        pullText:SetPoint("CENTER", pullBadge, "CENTER", 0, 0)
        pullText:SetTextColor(1, 1, 1)
        marker.pullText = pullText

        local highlight = marker:CreateTexture(nil, "HIGHLIGHT")
        highlight:SetPoint("CENTER")
        highlight:SetSize(22, 22)
        highlight:SetAtlas("UI-QuestPoi-OuterGlow", false)
        marker:SetHighlightTexture(highlight)

        marker:SetScript("OnEnter", ShowEnemyMarkerTooltip)
        marker:SetScript("OnLeave", GameTooltip_Hide)
        enemyMarkers[index] = marker
    end
    return enemyMarkers[index]
end

local function HideEnemyMarkers(firstUnused)
    for index = firstUnused, #enemyMarkers do
        enemyMarkers[index]:Hide()
    end
end

local function RefreshEnemyMarkers(mapID)
    if not Addon.db.showAdds then
        HideEnemyMarkers(1)
        return
    end

    local enemies = Addon.DungeonEnemies:GetEnemies(mapID)
    local route = Addon.Routes and Addon.Routes:GetRouteForMap(mapID)
    local markerIndex = 1
    for _, enemy in ipairs(enemies or {}) do
        local marker = AcquireEnemyMarker(markerIndex)
        marker.mapX = enemy.mapX
        marker.mapY = enemy.mapY
        marker.enemy = enemy
        marker.routeAssignment = Addon.Routes
            and Addon.Routes:GetPullAssignment(route, enemy.enemyIndex, enemy.cloneIndex)
        marker.killed = route
            and Addon.Routes:IsEnemyKilled(route, enemy.enemyIndex, enemy.cloneIndex)
            or false
        marker:SetFrameLevel(canvas:GetFrameLevel() + 16)
        local size = Addon:Clamp(math.floor((14 * (enemy.scale or 1)) + 0.5), 14, 24)
        marker:SetSize(size, size)
        marker.portrait:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark")
        marker.portrait:SetTexCoord(0.08, 0.92, 0.08, 0.92)
        if enemy.displayID and enemy.displayID > 0 then
            pcall(SetPortraitTextureFromCreatureDisplayID, marker.portrait, enemy.displayID)
        end
        marker.portrait:SetDesaturated(marker.killed)
        marker.portrait:SetAlpha(marker.killed and 0.45 or 1)
        local showForces = enemy.forces and enemy.forces > 0
        marker.badge:SetShown(showForces)
        marker.forceText:SetShown(showForces)
        marker.forceText:SetText(showForces and tostring(enemy.forces) or "")
        if marker.killed then
            marker.ring:SetVertexColor(0.34, 0.37, 0.4, 1)
            marker.pullBadge:SetVertexColor(0.34, 0.37, 0.4, 1)
            marker.pullText:SetTextColor(0.9, 0.92, 0.95)
            marker.pullBadge:Show()
            marker.pullText:SetText(tostring(marker.routeAssignment.pull))
            marker.pullText:Show()
        elseif marker.routeAssignment then
            local red, green, blue = Addon.Routes:GetColor(marker.routeAssignment.color)
            marker.ring:SetVertexColor(red, green, blue, 1)
            marker.pullBadge:SetVertexColor(red, green, blue, 1)
            marker.pullText:SetTextColor(Addon.Routes:GetTextColor(marker.routeAssignment.color))
            marker.pullBadge:Show()
            marker.pullText:SetText(tostring(marker.routeAssignment.pull))
            marker.pullText:Show()
        else
            marker.pullBadge:Hide()
            marker.pullText:Hide()
        end
        if not marker.routeAssignment then
            if enemy.interruptible then
                marker.ring:SetVertexColor(1, 0.48, 0.12, 1)
            elseif enemy.patrol then
                marker.ring:SetVertexColor(0.25, 0.65, 1, 1)
            else
                marker.ring:SetVertexColor(0.12, 0.82, 0.66, 1)
            end
        end
        PositionMarker(marker, enemy.mapX, enemy.mapY)
        markerIndex = markerIndex + 1
    end
    HideEnemyMarkers(markerIndex)
end

local function RefreshBossMarkers(mapID)
    if not Addon.db.showBosses then
        HideBossMarkers(1)
        return
    end

    local markerIndex = 1
    local dungeonIndex = Addon.DungeonEnemyData
        and Addon.DungeonEnemyData.maps
        and Addon.DungeonEnemyData.maps[mapID]
    local route = Addon.Routes and Addon.Routes:GetRouteForMap(mapID)
    if C_EncounterJournal.GetEncountersOnMap
        and (not CanShowEncounterJournal or CanShowEncounterJournal()) then
        local ok, encounters = pcall(C_EncounterJournal.GetEncountersOnMap, mapID)
        if ok then
            for _, encounterInfo in ipairs(encounters or {}) do
                local encounterID = encounterInfo.encounterID
                local x = encounterInfo.mapX
                local y = encounterInfo.mapY
                if encounterID and Addon:IsValidMapCoordinate(x, y) then
                    local name, description, _, _, _, instanceID = EJ_GetEncounterInfo(encounterID)
                    local displayInfo
                    local routeAssignment
                    for creatureIndex = 1, 10 do
                        local creatureOK, creatureName, _, creatureID, creatureDisplayID = pcall(
                            EJ_GetCreatureInfo,
                            creatureIndex,
                            encounterID
                        )
                        if not creatureOK or (not creatureName and not creatureID and not creatureDisplayID) then
                            break
                        end
                        displayInfo = displayInfo or creatureDisplayID
                        local assignment = Addon.Routes and Addon.Routes:GetBossPullAssignment(
                            route,
                            dungeonIndex,
                            creatureID,
                            creatureDisplayID
                        )
                        if assignment and (not routeAssignment or assignment.pull < routeAssignment.pull) then
                            routeAssignment = assignment
                        end
                    end
                    local completed = false
                    local completionOK, isComplete = pcall(
                        C_EncounterJournal.IsEncounterComplete,
                        encounterID
                    )
                    if completionOK then
                        completed = not not isComplete
                    end

                    local marker = AcquireBossMarker(markerIndex)
                    marker.mapX = x
                    marker.mapY = y
                    marker.encounterID = encounterID
                    marker.instanceID = instanceID
                    marker.title = name or L.DUNGEON_BOSS
                    marker.description = description
                    marker.completed = completed
                    marker.routeAssignment = routeAssignment
                    marker:SetFrameLevel(canvas:GetFrameLevel() + 17)
                    marker.portrait:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark")
                    marker.portrait:SetTexCoord(0.08, 0.92, 0.08, 0.92)
                    if displayInfo then
                        pcall(SetPortraitTextureFromCreatureDisplayID, marker.portrait, displayInfo)
                    end
                    marker.portrait:SetDesaturated(completed)
                    marker.portrait:SetAlpha(completed and 0.6 or 1)
                    marker.defeatedCross:SetShown(completed)
                    if routeAssignment then
                        local red, green, blue = Addon.Routes:GetColor(routeAssignment.color)
                        marker.pullBadge:SetVertexColor(red, green, blue, 1)
                        marker.pullText:SetTextColor(Addon.Routes:GetTextColor(routeAssignment.color))
                        marker.pullText:SetText(tostring(routeAssignment.pull))
                        marker.pullBadge:Show()
                        marker.pullText:Show()
                    else
                        marker.pullBadge:Hide()
                        marker.pullText:Hide()
                    end
                    PositionMarker(marker, x, y)
                    markerIndex = markerIndex + 1
                end
            end
        end
    end
    HideBossMarkers(markerIndex)
end

function Markers:SetBossesShown(shown)
    Addon.db.showBosses = not not shown
    if Addon.db.showBosses then
        self:RefreshContentMarkers()
    else
        HideBossMarkers(1)
    end
    if Addon.AddonButtons and Addon.AddonButtons.UpdateBossButton then
        Addon.AddonButtons:UpdateBossButton()
    end
end

function Markers:SetAddsShown(shown)
    Addon.db.showAdds = not not shown
    if Addon.db.showAdds then
        self:RefreshContentMarkers()
    else
        HideEnemyMarkers(1)
    end
    if Addon.AddonButtons and Addon.AddonButtons.UpdateAddsButton then
        Addon.AddonButtons:UpdateAddsButton()
    end
end

local function AcquireContentMarker(index)
    if not contentMarkers[index] then
        local marker = CreateFrame("Button", nil, canvas)
        marker:SetSize(22, 22)
        marker:RegisterForClicks("LeftButtonUp", "RightButtonUp")

        local glow = marker:CreateTexture(nil, "BACKGROUND")
        glow:SetPoint("CENTER")
        glow:SetSize(28, 28)
        glow:SetAtlas("UI-QuestPoi-OuterGlow", false)
        glow:SetAlpha(0.45)
        marker.glow = glow

        local ring = marker:CreateTexture(nil, "BORDER")
        ring:SetPoint("CENTER")
        ring:SetSize(24, 24)
        ring:Hide()
        marker.ring = ring

        local icon = marker:CreateTexture(nil, "ARTWORK")
        icon:SetPoint("CENTER")
        icon:SetSize(22, 22)
        marker.icon = icon

        marker:SetHighlightTexture("Interface\\Buttons\\ButtonHilight-Square", "ADD")
        marker:SetScript("OnEnter", ShowContentMarkerTooltip)
        marker:SetScript("OnLeave", GameTooltip_Hide)
        marker:SetScript("OnClick", TrackContentMarker)
        contentMarkers[index] = marker
    end

    return contentMarkers[index]
end

local function HideContentMarkers(firstUnused)
    for index = firstUnused, #contentMarkers do
        contentMarkers[index]:Hide()
    end
end

function Markers:RefreshContentMarkers()
    if not state.mapID or not state.layer then
        HideContentMarkers(1)
        HideBossMarkers(1)
        HideEnemyMarkers(1)
        return
    end

    local markerIndex = 1
    local usedKeys = {}

    RefreshBossMarkers(state.mapID)
    RefreshEnemyMarkers(state.mapID)

    local function AddMarker(key, x, y, atlasName, title, kindLabel, description, data)
        if usedKeys[key] or not Addon:IsValidMapCoordinate(x, y) then
            return
        end

        usedKeys[key] = true
        local marker = AcquireContentMarker(markerIndex)
        marker.mapX = x
        marker.mapY = y
        marker.title = title
        marker.kindLabel = kindLabel
        marker.description = description
        marker.questID = data and data.questID or nil
        marker.areaPoiID = data and data.areaPoiID or nil
        marker.linkedUiMapID = data and data.linkedUiMapID or nil
        marker.superTrackType = data and data.superTrackType or nil
        marker.superTrackID = data and data.superTrackID or nil
        marker.trackable = data and data.trackable or false
        marker.gossipPoiID = data and data.gossipPoiID or nil
        marker:SetFrameLevel(canvas:GetFrameLevel() + (data and data.level or 10))
        local markerSize = data and data.size or 22
        local iconScale = data and data.iconScale or 1
        marker:SetSize(markerSize, markerSize)
        marker.glow:SetSize(markerSize + 8, markerSize + 8)
        marker.glow:SetAtlas(data and data.glowAtlas or "UI-QuestPoi-OuterGlow", false)
        marker.glow:SetShown(not data or data.showGlow ~= false)
        marker.ring:ClearAllPoints()
        marker.ring:SetPoint("CENTER")
        marker.ring:SetSize(markerSize + 2, markerSize + 2)
        marker.ring:SetShown(
            data
                and data.ringAtlas
                and SetMarkerAtlas(marker.ring, data.ringAtlas)
                or false
        )
        marker.icon:ClearAllPoints()
        marker.icon:SetPoint("CENTER")
        marker.icon:SetSize(markerSize * iconScale, markerSize * iconScale)
        SetMarkerAtlas(marker.icon, atlasName, data and data.textureIndex, data and data.textureKit)
        PositionMarker(marker, x, y)
        markerIndex = markerIndex + 1
    end

    local dungeonEntrances = C_EncounterJournal.GetDungeonEntrancesForMap(state.mapID) or {}
    for _, info in ipairs(dungeonEntrances) do
        local x, y = Coordinates:SafeGetXY(info.position)
        if x and y then
            AddMarker(
                "areapoi:" .. tostring(info.areaPoiID), x, y, info.atlasName, info.name,
                L.DUNGEON_RAID_ENTRANCE, info.description,
                { areaPoiID = info.areaPoiID, level = 11, size = 22 }
            )
        end
    end

    local gossipPoiID, gossipInfo, gossipX, gossipY = self:GetGossipPOI(state.mapID)
    if gossipPoiID then
        AddMarker(
            "gossip:" .. tostring(gossipPoiID),
            gossipX,
            gossipY,
            nil,
            gossipInfo.name,
            L.GUARD_DIRECTIONS,
            nil,
            {
                level = 18,
                size = 24,
                textureIndex = gossipInfo.textureIndex,
                gossipPoiID = gossipPoiID,
            }
        )
    end

    local mapLinks = C_Map.GetMapLinksForMap(state.mapID) or {}
    for _, info in ipairs(mapLinks) do
        local x, y = Coordinates:SafeGetXY(info.position)
        if x and y then
            AddMarker(
                "areapoi:" .. tostring(info.areaPoiID), x, y, info.atlasName, info.name,
                L.ACCESS_OTHER_ZONE, nil,
                {
                    areaPoiID = info.areaPoiID,
                    linkedUiMapID = info.linkedUiMapID,
                    level = 11,
                    size = 22,
                    showGlow = false,
                }
            )
        end
    end

    if C_TaxiMap.ShouldMapShowTaxiNodes(state.mapID) then
        local factionGroup = UnitFactionGroup("player")
        local taxiNodes = C_TaxiMap.GetTaxiNodesForMap(state.mapID) or {}
        for _, info in ipairs(taxiNodes) do
            local correctFaction = info.faction == Enum.FlightPathFaction.Neutral
                or (info.faction == Enum.FlightPathFaction.Horde and factionGroup == "Horde")
                or (info.faction == Enum.FlightPathFaction.Alliance and factionGroup == "Alliance")
            local x, y = Coordinates:SafeGetXY(info.position)
            if correctFaction and x and y then
                AddMarker(
                    "taxi:" .. tostring(info.nodeID), x, y, info.atlasName, info.name,
                    info.isUndiscovered and L.UNDISCOVERED_FLIGHT_POINT or L.FLIGHT_POINT, nil,
                    {
                        superTrackType = Enum.SuperTrackingMapPinType.TaxiNode,
                        superTrackID = info.nodeID,
                        trackable = true,
                        level = 11,
                        size = 20,
                        showGlow = false,
                        textureKit = info.textureKit,
                    }
                )
            end
        end
    end

    if GetCVarBool("digSites") then
        local digSites = C_ResearchInfo.GetDigSitesForMap(state.mapID) or {}
        for _, info in ipairs(digSites) do
            local x, y = Coordinates:SafeGetXY(info.position)
            if x and y then
                AddMarker(
                    "digsite:" .. tostring(info.researchSiteID), x, y, nil, info.name,
                    L.ARCHAEOLOGY_DIG, nil,
                    {
                        superTrackType = Enum.SuperTrackingMapPinType.DigSite,
                        superTrackID = info.researchSiteID,
                        trackable = true,
                        level = 10,
                        size = 20,
                        showGlow = false,
                        textureIndex = info.textureIndex,
                    }
                )
            end
        end
    end

    local function AddAreaPOIList(areaPoiIDs, kindLabel, level, size, fallbackAtlas, useNativeRing)
        for _, areaPoiID in ipairs(areaPoiIDs or {}) do
            local info = C_AreaPoiInfo.GetAreaPOIInfo(state.mapID, areaPoiID)
            if info then
                local x, y = Coordinates:SafeGetXY(info.position)
                if x and y then
                    AddMarker(
                        "areapoi:" .. tostring(areaPoiID), x, y, info.atlasName or fallbackAtlas,
                        info.name, kindLabel, info.description,
                        {
                            areaPoiID = areaPoiID,
                            linkedUiMapID = info.linkedUiMapID,
                            level = level,
                            size = size,
                            showGlow = info.shouldGlow,
                            textureIndex = info.textureIndex,
                            textureKit = info.uiTextureKit,
                            ringAtlas = useNativeRing and "worldquest-questmarker-epic" or nil,
                            iconScale = useNativeRing and 0.68 or 1,
                        }
                    )
                end
            end
        end
    end

    AddAreaPOIList(C_AreaPoiInfo.GetEventsForMap(state.mapID), L.EVENT, 12, 25, "poi-workorders", true)
    AddAreaPOIList(C_AreaPoiInfo.GetDelvesForMap(state.mapID), L.DELVE, 12, 25, "delves-bountiful", true)
    AddAreaPOIList(C_AreaPoiInfo.GetDragonridingRacesForMap(state.mapID), L.SKYRIDING_RACE, 11, 24, "racing", true)
    AddAreaPOIList(C_AreaPoiInfo.GetQuestHubsForMap(state.mapID), L.QUEST_HUB, 11, 23, "QuestNormal", false)

    local areaPOIs = C_AreaPoiInfo.GetAreaPOIForMap(state.mapID) or {}
    for _, areaPoiID in ipairs(areaPOIs) do
        local info = C_AreaPoiInfo.GetAreaPOIInfo(state.mapID, areaPoiID)
        if info then
            local x, y = Coordinates:SafeGetXY(info.position)
            if x and y then
                AddMarker(
                    "areapoi:" .. tostring(areaPoiID), x, y, info.atlasName, info.name,
                    info.isCurrentEvent and L.CURRENT_EVENT or L.POINT_OF_INTEREST, info.description,
                    {
                        areaPoiID = areaPoiID,
                        linkedUiMapID = info.linkedUiMapID,
                        level = 10,
                        size = 24,
                        showGlow = info.shouldGlow,
                        textureIndex = info.textureIndex,
                        textureKit = info.uiTextureKit,
                        ringAtlas = "worldquest-questmarker-epic",
                        iconScale = 0.68,
                    }
                )
            end
        end
    end

    local vignetteGUIDs = C_VignetteInfo.GetVignettes() or {}
    for _, vignetteGUID in ipairs(vignetteGUIDs) do
        local info = C_VignetteInfo.GetVignetteInfo(vignetteGUID)
        if info and info.onWorldMap and not info.isDead then
            local position = C_VignetteInfo.GetVignettePosition(vignetteGUID, state.mapID)
            local x, y = Coordinates:SafeGetXY(position)
            if x and y then
                local label = info.type == Enum.VignetteType.Treasure and L.TREASURE or L.RARE_HIGHLIGHT
                AddMarker(
                    "vignette:" .. tostring(vignetteGUID), x, y, info.atlasName, info.name,
                    label, nil, { level = 12, size = 20, showGlow = false }
                )
            end
        end
    end

    local function AddAvailableQuest(info)
        local questID = info and info.questID
        if not questID or info.inProgress or C_QuestLog.IsWorldQuest(questID) then
            return false
        end

        local atlasName, classification, glowAtlas = GetAvailableQuestVisual(info)
        AddMarker(
            "quest:" .. tostring(questID), info.x, info.y, atlasName,
            info.questName or info.questLineName or GetQuestTitle(questID),
            GetAvailableQuestLabel(classification, info),
            info.questLineName,
            { questID = questID, level = 14, size = 24, glowAtlas = glowAtlas }
        )
        return true
    end

    C_QuestLine.RequestQuestLinesForMap(state.mapID)
    local availableQuestLines = C_QuestLine.GetAvailableQuestLines(state.mapID) or {}
    for _, info in ipairs(availableQuestLines) do
        AddAvailableQuest(info)
    end

    if C_QuestLine.GetForceVisibleQuests and C_QuestLine.GetQuestLineInfo then
        local forceVisibleQuests = C_QuestLine.GetForceVisibleQuests(state.mapID) or {}
        for _, questID in ipairs(forceVisibleQuests) do
            AddAvailableQuest(C_QuestLine.GetQuestLineInfo(questID, state.mapID))
        end
    end

    local tasks = C_TaskQuest.GetQuestsOnMap(state.mapID) or {}
    for _, info in ipairs(tasks) do
        local questID = info.questID
        if questID and C_QuestLog.IsWorldQuest(questID) then
            local classification = GetQuestClassification(questID)
            AddMarker(
                "quest:" .. tostring(questID), info.x, info.y, GetWorldQuestAtlas(questID, info.inProgress),
                GetQuestTitle(questID), L.WORLD_QUEST, nil,
                {
                    questID = questID,
                    trackable = true,
                    level = 16,
                    size = 26,
                    ringAtlas = GetQuestRingAtlas(classification, false),
                    glowAtlas = GetQuestGlowAtlas(classification),
                    iconScale = 0.68,
                }
            )
        elseif questID and not info.inProgress then
            AddAvailableQuest(info)
        elseif questID then
            AddMarker(
                "quest:" .. tostring(questID), info.x, info.y, "Bonus-Objective-Star",
                GetQuestTitle(questID), L.BONUS_OBJECTIVE, nil,
                {
                    questID = questID,
                    trackable = true,
                    level = 13,
                    size = 24,
                    ringAtlas = "worldquest-questmarker-epic",
                    iconScale = 0.64,
                }
            )
        end
    end

    local activeQuests = C_QuestLog.GetQuestsOnMap(state.mapID) or {}
    for _, info in ipairs(activeQuests) do
        local questID = info.questID
        if questID and not C_QuestLog.IsWorldQuest(questID) then
            local isComplete = C_QuestLog.IsComplete(questID)
            local atlasName, classification, ringAtlas, glowAtlas, standaloneIcon = GetActiveQuestVisual(questID)
            local kindLabel = isComplete and L.QUEST_TURN_IN or L.ACTIVE_QUEST
            if not isComplete and IsQuestClassification(classification, "Recurring") then
                kindLabel = L.RECURRING_QUEST
            end
            AddMarker(
                "quest:" .. tostring(questID), info.x, info.y, atlasName,
                GetQuestTitle(questID), kindLabel, nil,
                {
                    questID = questID,
                    trackable = true,
                    level = 15,
                    size = 24,
                    ringAtlas = ringAtlas,
                    glowAtlas = glowAtlas,
                    iconScale = standaloneIcon and 1 or 0.66,
                }
            )
        end
    end

    HideContentMarkers(markerIndex)
    self:UpdateShapeClipping()
end

function Markers:ScheduleContentRefresh()
    if state.contentRefreshPending then
        return
    end

    state.contentRefreshPending = true
    C_Timer.After(0.15, function()
        state.contentRefreshPending = false
        if mapFrame and mapFrame:IsShown() then
            self:RefreshContentMarkers()
        end
    end)
end

function Markers:UpdateGroupMarkers()
    local markerIndex = 1
    local units = {}

    if IsInRaid() then
        for index = 1, GetNumGroupMembers() do
            units[#units + 1] = "raid" .. index
        end
    elseif IsInGroup() then
        for index = 1, GetNumSubgroupMembers() do
            units[#units + 1] = "party" .. index
        end
    end

    for _, unit in ipairs(units) do
        if UnitExists(unit) and not UnitIsUnit(unit, "player") then
            local x, y = Coordinates:GetUnitMapPosition(state.mapID, unit)
            if x and y then
                local marker = AcquireGroupMarker(markerIndex)
                local _, class = UnitClass(unit)
                if Addon:IsSecret(class) or type(class) ~= "string" then
                    class = nil
                end
                local color = class and RAID_CLASS_COLORS[class]
                if color then
                    marker:SetBackdropColor(color.r, color.g, color.b, 1)
                else
                    marker:SetBackdropColor(0.3, 0.8, 1, 1)
                end
                PositionMarker(marker, x, y)
                markerIndex = markerIndex + 1
            end
        end
    end

    HideGroupMarkers(markerIndex)
end

function Markers:UpdateCorpseMarker()
    if not corpseMarker
        or not state.mapID
        or not C_DeathInfo
        or not C_DeathInfo.GetCorpseMapPosition
    then
        if corpseMarker then
            corpseMarker:Hide()
        end
        return false
    end

    local ok, position = pcall(C_DeathInfo.GetCorpseMapPosition, state.mapID)
    local x, y
    if ok then
        x, y = Coordinates:SafeGetXY(position)
    end
    if not x or not y then
        corpseMarker:Hide()
        return false
    end

    PositionMarker(corpseMarker, x, y)
    return true
end

function Markers:UpdateMarkers()
    if not state.mapID or not state.layer then
        playerMarker:Hide()
        waypointMarker:Hide()
        corpseMarker:Hide()
        if nativePlayerFrame then
            nativePlayerFrame:Hide()
        end
        HideGroupMarkers(1)
        return
    end

    local playerX, playerY = Coordinates:GetUnitMapPosition(state.mapID, "player")
    self:UpdateCorpseMarker()
    if self:UpdateGossipArrival(playerX, playerY) then
        self:ScheduleContentRefresh()
    end
    if Addon.Gathering then
        Addon.Gathering:UpdateNativeBlips(playerX, playerY)
    end
    if nativePlayerFrame then
        if nativeMapID ~= state.mapID then
            local ok = pcall(nativePlayerFrame.SetUiMapID, nativePlayerFrame, state.mapID)
            if ok then
                nativeMapID = state.mapID
            end
        end

        nativePlayerFrame:Show()
        playerMarker:Hide()
        HideGroupMarkers(1)

        if playerX and playerY then
            statusText:SetFormattedText(
                "%.1f, %.1f  |  Zoom %d/%d",
                playerX * 100,
                playerY * 100,
                Addon.db.zoom,
                #Addon.ZOOM_LEVELS
            )
        else
            local positionStatus = state.protectedCenterMode == "vista completa automática"
                and L.FULL_OVERVIEW
                or L.PROTECTED_POSITION
            statusText:SetFormattedText(
                positionStatus .. "  |  Zoom %d/%d",
                Addon.db.zoom,
                #Addon.ZOOM_LEVELS
            )
        end
    elseif playerX and playerY then
        PositionMarker(playerMarker, playerX, playerY)
        local facing = GetPlayerFacing()
        if not Addon:IsSecret(facing) and type(facing) == "number" then
            playerMarker.arrow:SetRotation(facing)
        end
        statusText:SetFormattedText(
            "%.1f, %.1f  |  Zoom %d/%d",
            playerX * 100,
            playerY * 100,
            Addon.db.zoom,
            #Addon.ZOOM_LEVELS
        )
    else
        playerMarker:Hide()
        statusText:SetFormattedText(
            L.POSITION_UNAVAILABLE,
            Addon.db.zoom,
            #Addon.ZOOM_LEVELS
        )
    end

    local waypointX
    local waypointY
    if C_Map.HasUserWaypoint() then
        local ok, position = pcall(C_Map.GetUserWaypointPositionForMap, state.mapID)
        if ok then
            waypointX, waypointY = Coordinates:SafeGetXY(position)
        end
    end

    if waypointX and waypointY then
        PositionMarker(waypointMarker, waypointX, waypointY)
    else
        waypointMarker:Hide()
    end

    if not nativePlayerFrame then
        self:UpdateGroupMarkers()
    end
    self:UpdateShapeClipping()
end

function Markers:RepositionContentMarkers()
    for _, marker in ipairs(contentMarkers) do
        if marker:IsShown() and marker.mapX and marker.mapY then
            PositionMarker(marker, marker.mapX, marker.mapY)
        end
    end
    for _, marker in ipairs(bossMarkers) do
        if marker:IsShown() and marker.mapX and marker.mapY then
            PositionMarker(marker, marker.mapX, marker.mapY)
        end
    end
    for _, marker in ipairs(enemyMarkers) do
        if marker:IsShown() and marker.mapX and marker.mapY then
            PositionMarker(marker, marker.mapX, marker.mapY)
        end
    end
    self:UpdateShapeClipping()
end

function Markers:UpdateShapeClipping()
    ApplyShapeVisibility(playerMarker)
    ApplyShapeVisibility(waypointMarker)
    ApplyShapeVisibility(corpseMarker)
    for _, marker in ipairs(groupMarkers) do
        ApplyShapeVisibility(marker)
    end
    for _, marker in ipairs(contentMarkers) do
        ApplyShapeVisibility(marker)
    end
    for _, marker in ipairs(bossMarkers) do
        ApplyShapeVisibility(marker)
    end
    for _, marker in ipairs(enemyMarkers) do
        ApplyShapeVisibility(marker)
    end
    ApplyShapeToChildren(nativePlayerFrame, 3)
end

function Markers:HideAll()
    HideContentMarkers(1)
    HideBossMarkers(1)
    HideEnemyMarkers(1)
    HideGroupMarkers(1)
    if playerMarker then
        playerMarker:Hide()
    end
    if nativePlayerFrame then
        nativePlayerFrame:Hide()
    end
    if waypointMarker then
        waypointMarker:Hide()
    end
    if corpseMarker then
        corpseMarker:Hide()
    end
end

function Markers:IsNativePlayerFrameAvailable()
    return nativePlayerFrame ~= nil
end

function Markers:IsPlayerUnderMouse()
    if not nativePlayerFrame or not nativePlayerFrame:IsShown() then
        return false
    end

    local results = { pcall(nativePlayerFrame.GetMouseOverUnits, nativePlayerFrame) }
    if not results[1] then
        return false
    end

    for index = 2, #results do
        local unit = results[index]
        if unit == "player" then
            return true
        end
        local ok, isPlayer = pcall(UnitIsUnit, unit, "player")
        if ok and isPlayer then
            return true
        end
    end
    return false
end

function Markers:HighlightPlayer()
    if not nativePlayerFrame or not state.mapID then
        return
    end

    pcall(function()
        if nativeMapID ~= state.mapID then
            nativePlayerFrame:SetUiMapID(state.mapID)
            nativeMapID = state.mapID
        end
        nativePlayerFrame:UpdatePlayerPins()
        nativePlayerFrame:SetPlayerPingScale(math.max(1, Addon.db.playerArrowSize / 18))
        nativePlayerFrame:StartPlayerPing(2.5, 0.6)
    end)
end

function Markers:SetPlayerArrowSize(size)
    size = Addon:Clamp(
        math.floor((tonumber(size) or Addon.defaults.playerArrowSize) + 0.5),
        Addon.MIN_ARROW_SIZE,
        Addon.MAX_ARROW_SIZE
    )

    if playerMarker then
        playerMarker:SetSize(size, size)
    end
    if nativePlayerFrame then
        pcall(nativePlayerFrame.SetPinSize, nativePlayerFrame, "player", size)
    end
end

function Markers:Initialize(ui)
    canvas = ui.canvas
    viewport = ui.viewport
    mapFrame = ui.mapFrame
    statusText = ui.statusText
    CreatePrimaryMarkers()
end
