---@class AddonPrivate
local Private = select(2, ...)

---@class NearbyHud
local hud = {}
Private.NearbyHud = hud

local const = Private.constants

local WIDTH        = 290
local ROW_HEIGHT   = 22
local BODY_TOP     = 62   -- y offset (from top) where the objective rows begin
local MAX_ROWS_CAP = 12
local REFRESH_INTERVAL = 1.0

-- Same parchment + gold border as the main window (see BaseFrame.lua).
local FrameBackdrop = {
    bgFile   = "interface\\achievementframe\\ui-guildachievement-parchment",
    edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Gold-Border",
    edgeSize = 16,
    insets   = { left = 4, right = 4, top = 4, bottom = 4 },
}

local O = Private.Objectives

-- Thin delegations to the shared Objectives module (single source of truth).
local TYPE_ICONS  = O.TYPE_ICONS
local TYPE_LABELS = O.TYPE_LABELS
local function ClassifyCriterion(ach, crit)       return O:Classify(ach, crit) end
local function IsAchievementComplete(id)          return O:IsAchievementComplete(id) end
local function IsCriterionComplete(ach, crit)     return O:IsCriterionComplete(ach, crit) end
local function GetDistance(mapID, px, py, nx, ny)  return O:Distance(mapID, px, py, nx, ny) end
local function FormatDistance(realYards)          return O:FormatDistance(realYards) end

function hud:InvalidateIndex() O:InvalidateIndex() end

-- ---------------------------------------------------------------------------
-- Rows
-- ---------------------------------------------------------------------------
-- Is this row's objective the one currently being tracked?
local function IsRowActive(d)
    local active = Private.WaypointsUtil:GetActiveTarget()
    return active ~= nil
        and active.achievement == d.ach
        and active.criterion == d.crit
        and active.mapID == d.wp.mapID
        and active.x == d.wp.x
        and active.y == d.wp.y
end

local function OnRowClick(row, button)
    if not row.data then return end
    local d = row.data
    if button == "RightButton" then
        -- Skip this objective for now (saved to the ignore list).
        O:SetIgnored(d.ach, d.crit, d.wp, true)
        Private.NearbyHud:Refresh()
        if Private.RouteHud then Private.RouteHud:Refresh() end
        return
    end
    if IsRowActive(d) then
        -- Clicking the already-tracked objective clears it (quick un-track).
        Private.WaypointsUtil:ClearActiveTarget()
        Private.NearbyHud:Refresh()
    else
        Private.WaypointsUtil:TrackWaypoint(d.ach, d.crit, d.wp)
    end
end

local function OnRowEnter(row)
    if not row.data then return end
    local d = row.data
    GameTooltip:SetOwner(row, "ANCHOR_LEFT")
    local header = d.ach.name
    if d.ctype and TYPE_LABELS[d.ctype] then
        header = "|cffffd700[" .. TYPE_LABELS[d.ctype] .. "]|r " .. header
    end
    GameTooltip:AddLine(header, 1, 0.82, 0)
    GameTooltip:AddLine(d.crit.description or "", 1, 1, 1, true)
    if d.crit.guide and d.crit.guide ~= "" then
        GameTooltip:AddLine(" ")
        GameTooltip:AddLine("Guide:", 1, 0.84, 0)
        GameTooltip:AddLine(d.crit.guide, 0.8, 1, 0.8, true)
    else
        local hint = O:ContextHint(d.ctype)
        if hint then
            GameTooltip:AddLine(" ")
            GameTooltip:AddLine(hint, 0.8, 1, 0.8, true)
        end
    end
    GameTooltip:AddLine(" ")
    if IsRowActive(d) then
        GameTooltip:AddLine("Click to clear this waypoint.", 1, 0.5, 0)
    else
        GameTooltip:AddLine("Click to set a waypoint.", 0, 1, 0)
    end
    GameTooltip:AddLine("Right-click to ignore (skip) this objective.", 0.7, 0.7, 0.7)
    GameTooltip:Show()
end

local function AcquireRow(f, index)
    if f.rows[index] then return f.rows[index] end
    local y = -(BODY_TOP + (index - 1) * ROW_HEIGHT)

    local row = CreateFrame("Button", nil, f)
    row:SetHeight(ROW_HEIGHT)
    row:SetPoint("TOPLEFT", f, "TOPLEFT", 12, y)
    row:SetPoint("TOPRIGHT", f, "TOPRIGHT", -12, y)

    row.icon = row:CreateTexture(nil, "ARTWORK")
    row.icon:SetSize(16, 16)
    row.icon:SetPoint("LEFT", 0, 0)

    row.dist = row:CreateFontString(nil, "OVERLAY")
    row.dist:SetFontObject(const.FONT_OBJECTS.SMALL)
    row.dist:SetPoint("RIGHT", 0, 0)
    row.dist:SetJustifyH("RIGHT")
    row.dist:SetWidth(46)

    row.label = row:CreateFontString(nil, "OVERLAY")
    row.label:SetFontObject(const.FONT_OBJECTS.SMALL)
    row.label:SetPoint("LEFT", row.icon, "RIGHT", 5, 0)
    row.label:SetPoint("RIGHT", row.dist, "LEFT", -4, 0)
    row.label:SetJustifyH("LEFT")
    row.label:SetWordWrap(false)

    local hl = row:CreateTexture(nil, "HIGHLIGHT")
    hl:SetAllPoints()
    hl:SetColorTexture(1, 0.85, 0, 0.15)

    row:RegisterForClicks("LeftButtonUp", "RightButtonUp")
    row:SetScript("OnClick", OnRowClick)
    row:SetScript("OnEnter", OnRowEnter)
    row:SetScript("OnLeave", function() GameTooltip:Hide() end)

    f.rows[index] = row
    return row
end

-- ---------------------------------------------------------------------------
-- Travel hint
-- ---------------------------------------------------------------------------
-- Conservatively report whether the tracked target's pin has been removed.
-- Returns true ONLY when we can positively confirm it's gone (so an unexpected
-- TomTom layout never causes a false "cleared" and hides a valid track).
local function TrackedWaypointGone(target)
    if target.tomtomUID and TomTom then
        local store = TomTom.waypoints
        if type(store) ~= "table" then return false end -- unknown structure: don't clear
        for _, mapWps in pairs(store) do
            if type(mapWps) == "table" then
                for _, wp in pairs(mapWps) do
                    if wp == target.tomtomUID then return false end
                end
            end
        end
        return true -- uid no longer present anywhere in TomTom's store
    elseif target.usedNative and C_Map.GetUserWaypoint then
        return C_Map.GetUserWaypoint() == nil
    end
    return false
end

function hud:UpdateTravelHint()
    local f = self.frame
    if not f then return end
    local s = Private.Settings:Hud()
    if not s.showTravelHint then
        f.hint:SetText("")
        return
    end
    local target = Private.WaypointsUtil:GetActiveTarget()
    -- Auto-clear the tracked target (and its arrow) once its objective is complete.
    if target and target.achievement and target.criterion then
        if IsAchievementComplete(target.achievement.achievementID)
            or IsCriterionComplete(target.achievement, target.criterion) then
            Private.WaypointsUtil:ClearActiveTarget()
            target = nil
        end
    end
    -- Also clear if the underlying pin was removed externally (e.g. you cleared
    -- TomTom's waypoints, or removed the native user waypoint).
    if target and TrackedWaypointGone(target) then
        Private.WaypointsUtil:ClearActiveTarget()
        target = nil
    end
    if not target then
        f.hint:SetText("|cff888888No active waypoint — click an objective below to track it.|r")
        return
    end
    local mapID = C_Map.GetBestMapForUnit("player")
    if mapID == target.mapID then
        local dtxt = ""
        local pos = C_Map.GetPlayerMapPosition(mapID, "player")
        if pos then
            local px, py = pos:GetXY()
            local _, yards = GetDistance(mapID, px, py, target.x / 100, target.y / 100)
            local ftxt = FormatDistance(yards)
            if ftxt ~= "" then dtxt = " — " .. ftxt end
        end
        f.hint:SetText(string.format("|cff00ff00Tracking:|r %s%s", target.name, dtxt))
    else
        f.hint:SetText(string.format("|cffffd700Travel to:|r %s |cffaaaaaa(%s)|r", target.zoneName, target.name))
    end
end

-- ---------------------------------------------------------------------------
-- Refresh
-- ---------------------------------------------------------------------------
function hud:Refresh()
    local f = self.frame
    if not f or not f:IsShown() then return end

    self:UpdateTravelHint()

    local s = Private.Settings:Hud()
    local maxRows = math.min(s.maxRows or 8, MAX_ROWS_CAP)

    local results = {}
    local mapID = C_Map.GetBestMapForUnit("player")
    local items = mapID and O:GetIncompleteOnMap(mapID)
    if items and #items > 0 then
        local pos = C_Map.GetPlayerMapPosition(mapID, "player")
        local px, py = 0.5, 0.5
        if pos then px, py = pos:GetXY() end
        for _, e in ipairs(items) do
            local sortKey, yards = GetDistance(mapID, px, py, e.wp.x / 100, e.wp.y / 100)
            tinsert(results, { ach = e.ach, crit = e.crit, wp = e.wp, sortKey = sortKey, yards = yards })
        end
        table.sort(results, function(a, b) return a.sortKey < b.sortKey end)
    end

    local shownCount = 0
    for i = 1, maxRows do
        local data = results[i]
        if data then
            local row = AcquireRow(f, i)
            row.data = data
            local ctype = ClassifyCriterion(data.ach, data.crit)
            data.ctype = ctype
            if not O:ApplyTypeIcon(row.icon, ctype) then
                row.icon:SetTexCoord(0, 1, 0, 1)
                row.icon:SetTexture(select(10, GetAchievementInfo(data.ach.achievementID))
                    or "Interface\\Icons\\INV_Misc_QuestionMark")
            end
            row.label:SetText(data.crit.description or data.ach.name)
            row.dist:SetText(FormatDistance(data.yards))
            row:Show()
            shownCount = i
        elseif f.rows[i] then
            f.rows[i].data = nil
            f.rows[i]:Hide()
        end
    end

    if shownCount == 0 then
        f.empty:Show()
        f:SetHeight(BODY_TOP + 52)
    else
        f.empty:Hide()
        f:SetHeight(BODY_TOP + shownCount * ROW_HEIGHT + 8)
    end
end

-- ---------------------------------------------------------------------------
-- Ticker (only runs while the HUD is visible)
-- ---------------------------------------------------------------------------
function hud:StartTicker()
    if self.ticker then return end
    self.ticker = C_Timer.NewTicker(REFRESH_INTERVAL, function() hud:Refresh() end)
end

function hud:StopTicker()
    if self.ticker then
        self.ticker:Cancel()
        self.ticker = nil
    end
end

-- ---------------------------------------------------------------------------
-- Enable / toggle
-- ---------------------------------------------------------------------------
function hud:IsEnabled()
    local s = Private.Settings:Hud()
    return s and s.enabled
end

function hud:SetEnabled(enabled)
    local s = Private.Settings:Hud()
    if not s then return end
    s.enabled = enabled and true or false
    if not self.frame then self:Initialize() end
    if s.enabled then
        self.frame:Show()
        self:Refresh()
    else
        self.frame:Hide()
    end
end

function hud:Toggle()
    self:SetEnabled(not self:IsEnabled())
end

-- ---------------------------------------------------------------------------
-- Construction
-- ---------------------------------------------------------------------------
function hud:Initialize()
    if self.frame then return end
    local s = Private.Settings:Hud()

    local f = CreateFrame("Frame", "MetaTrackerNearbyHud", UIParent, "BackdropTemplate")
    f:SetSize(WIDTH, 120)
    f:SetBackdrop(FrameBackdrop)
    f:SetBackdropBorderColor(1, 1, 1, 1)
    f:SetFrameStrata("MEDIUM")
    f:SetClampedToScreen(true)
    f:SetMovable(true)
    f:EnableMouse(true)
    f:RegisterForDrag("LeftButton")
    f:SetScript("OnDragStart", function(self)
        if not Private.Settings:Hud().locked then self:StartMoving() end
    end)
    f:SetScript("OnDragStop", function(self)
        self:StopMovingOrSizing()
        local hs = Private.Settings:Hud()
        local point, _, relPoint, x, y = self:GetPoint()
        hs.point, hs.relPoint, hs.x, hs.y = point, relPoint, x, y
    end)

    -- header bar + title
    local titleBar = f:CreateTexture(nil, "ARTWORK")
    titleBar:SetTexture("Interface\\AchievementFrame\\UI-Achievement-Borders")
    titleBar:SetPoint("TOPLEFT", 10, -9)
    titleBar:SetPoint("TOPRIGHT", -10, -9)
    titleBar:SetHeight(22)
    titleBar:SetVertexColor(1, 1, 1, 0.8)
    titleBar:SetTexCoord(0, 0.9765625, 0.66015625, 0.73828125)

    local title = f:CreateFontString(nil, "OVERLAY")
    title:SetFontObject(const.FONT_OBJECTS.HEADING)
    title:SetPoint("TOP", titleBar, "TOP", 0, -3)
    title:SetText("Nearby Now")

    local close = CreateFrame("Button", nil, f, "UIPanelCloseButton")
    close:SetPoint("TOPRIGHT", 3, 3)
    close:SetScale(0.8)
    close:SetScript("OnClick", function() hud:SetEnabled(false) end)

    -- travel hint line
    local hint = f:CreateFontString(nil, "OVERLAY")
    hint:SetFontObject(const.FONT_OBJECTS.SMALL)
    hint:SetPoint("TOPLEFT", 12, -36)
    hint:SetPoint("TOPRIGHT", -12, -36)
    hint:SetJustifyH("LEFT")
    hint:SetHeight(20)
    hint:SetWordWrap(true)
    f.hint = hint

    -- empty-state text
    local empty = f:CreateFontString(nil, "OVERLAY")
    empty:SetFontObject(const.FONT_OBJECTS.SMALL)
    empty:SetPoint("TOPLEFT", 12, -(BODY_TOP + 2))
    empty:SetPoint("TOPRIGHT", -12, -(BODY_TOP + 2))
    empty:SetJustifyH("LEFT")
    empty:SetSpacing(2)
    empty:SetText("|cff00ff00Meta-Tracker active|r \226\128\148 no tracked objectives on this map. Head to a Khaz Algar (The War Within) zone.")
    empty:Hide()
    f.empty = empty

    f.rows = {}
    self.frame = f

    f:ClearAllPoints()
    f:SetPoint(s.point or "CENTER", UIParent, s.relPoint or "CENTER", s.x or 0, s.y or 0)

    f:SetScript("OnShow", function() hud:StartTicker() end)
    f:SetScript("OnHide", function() hud:StopTicker() end)

    if s.enabled then
        f:Show()
        self:Refresh()
    else
        f:Hide()
    end
end
