-- ============================================================
-- RealNibba.MapFeatures  —  Player Trail + Minimap Node Flash
-- ============================================================

local P = RNPack

local TRAIL_TEX = "Interface\\AddOns\\RealNibba.Pack\\Images\\IconCircleFade"
local BLIP_NORM = "Interface\\AddOns\\RealNibba.Pack\\Images\\MMOIcons"
local BLIP_GLOW = "Interface\\AddOns\\RealNibba.Pack\\Images\\MMOIconsG"

-- LibMapData-1.0: accurate zone/dungeon dimensions for every map
local LMD = nil
local LMD_DEFAULT_W = 4741.67
local LMD_DEFAULT_H = 3160.42

local function GetZoneSize()
    if LMD then
        local w, h = LMD:MapArea(GetMapInfo(), GetCurrentMapDungeonLevel())
        if w and w > 0 then return w, h end
    end
    return LMD_DEFAULT_W, LMD_DEFAULT_H
end

-- ============================================================
-- MINIMAP ZOOM RADIUS  (yards, indexed by Minimap:GetZoom()+1)
-- ============================================================
local MM_RADII_OUTSIDE = { 233.33, 200, 166.67, 133.33, 100, 66.67 }
local MM_RADII_INSIDE  = { 150,    120, 90,      60,     40,  25    }
local mmIsInside       = false

local function GetMMRadius()
    local zoom = Minimap:GetZoom()
    return (mmIsInside and MM_RADII_INSIDE or MM_RADII_OUTSIDE)[zoom + 1] or 133.33
end

-- ============================================================
-- MINIMAP SHAPE  (supports ElvUI / SexyMap square minimaps)
-- ============================================================
local function MinimapIsRound()
    local shape = GetMinimapShape and GetMinimapShape() or "ROUND"
    return shape == "ROUND"
end

-- ============================================================
-- SECTION 1 — PLAYER TRAIL
-- ============================================================

local trailBuf    = {}
local trailHead   = 0
local trailCount  = 0
local trailZoneID = -1

local lastX, lastY = nil, nil

-- Map context ("current zone" state used by GetPlayerMapPosition) only goes
-- stale after the world map is browsed to a different zone and then closed.
-- SetMapToCurrentZone() is expensive on this server, so only call it when we
-- know the context is actually stale, instead of every tick.
local mapContextDirty = true

local function EnsureMapContext()
    if mapContextDirty then
        SetMapToCurrentZone()
        mapContextDirty = false
    end
end

local mmPool   = {}
local mmActive = {}

-- Plain BLEND mode — matches Carbonite, no glow
local function MakeDot()
    local d = CreateFrame("Frame", nil, Minimap)
    d.tex = d:CreateTexture(nil, "OVERLAY")
    d.tex:SetAllPoints(d)
    d.tex:SetTexture(TRAIL_TEX)
    return d
end

local function GetMMDot()
    local d = table.remove(mmPool)
    if d then d:Show(); return d end
    return MakeDot()
end

local function RecycleList(list, pool)
    for i = 1, #list do
        list[i]:Hide()
        pool[#pool + 1] = list[i]
        list[i] = nil
    end
end

local function Dist2(x1, y1, x2, y2)
    local dx, dy = x2-x1, y2-y1
    return dx*dx + dy*dy
end

-- ── Recording ─────────────────────────────────────────────────────────────

local function RecordPoint(x, y, zoneID)
    local db = P.DB and P.DB.mapTrail
    if not db then return end
    local maxCnt = db.dotCount
    if maxCnt < 1 then return end

    if zoneID ~= trailZoneID then
        trailBuf = {}; trailHead = 0; trailCount = 0
        trailZoneID = zoneID
    end

    if #trailBuf < maxCnt then
        for i = #trailBuf + 1, maxCnt do trailBuf[i] = false end
    end

    trailHead = (trailHead % maxCnt) + 1
    trailBuf[trailHead] = { x=x, y=y, t=GetTime() }
    if trailCount < maxCnt then trailCount = trailCount + 1 end
end

-- ── Minimap Drawing ────────────────────────────────────────────────────────

local function DrawMinimap()
    RecycleList(mmActive, mmPool)
    local db = P.DB and P.DB.mapTrail
    if not db or not db.minimapEnabled then return end
    if trailCount == 0 then return end

    -- Don't call SetMapToCurrentZone while the world map is visible —
    -- it causes C-level tile reloads. Minimap trail pauses while map is open.
    if WorldMapFrame:IsVisible() then return end

    EnsureMapContext()
    local px, py = GetPlayerMapPosition("player")
    if not px or (px == 0 and py == 0) then
        -- Flag said context was fine but position is bogus — force a refresh
        -- once and retry (covers cases we didn't catch via OnHide/zone events).
        mapContextDirty = true
        EnsureMapContext()
        px, py = GetPlayerMapPosition("player")
        if not px or (px == 0 and py == 0) then return end
    end
    local zoneW, zoneH  = GetZoneSize()
    local radius        = GetMMRadius()
    local diameter      = radius * 2
    -- Single size for both axes — fx/fy are in physical-yard fractions,
    -- so using separate mmW/mmH breaks symmetry on non-square frames.
    local mmSize        = math.min(Minimap:GetWidth(), Minimap:GetHeight())
    local fadeT         = db.fadeTime
    local size          = math.max(4, db.dotSize - 2)
    local c             = db.color
    local opac          = db.opacity or 1.0
    local now           = GetTime()
    local maxCnt        = db.dotCount
    local step          = size * 0.65

    local mmRound     = MinimapIsRound()
    local CLIP_CIRCLE = 0.1849
    local CLIP_RECT   = 0.46

    local function inBounds(fx, fy)
        if mmRound then
            return fx*fx + fy*fy < CLIP_CIRCLE
        else
            return math.abs(fx) < CLIP_RECT and math.abs(fy) < CLIP_RECT
        end
    end

    local start  = (trailHead - trailCount + maxCnt) % maxCnt
    local prevPt = nil
    local prevFX, prevFY, prevAlpha

    for i = 0, trailCount - 1 do
        local idx = (start + i) % maxCnt + 1
        local pt  = trailBuf[idx]
        if pt then
            local age = now - pt.t
            if age < fadeT then
                local alpha = (fadeT - age) / fadeT * opac
                local fx = (pt.x - px) * zoneW / diameter
                local fy = (pt.y - py) * zoneH / diameter

                if prevPt and prevAlpha then
                    local dfx  = (fx - prevFX) * mmSize
                    local dfy  = (fy - prevFY) * mmSize
                    local dist = math.sqrt(dfx*dfx + dfy*dfy)
                    if dist > step then
                        local n = math.floor(dist / step)
                        for k = 1, n - 1 do
                            local t    = k / n
                            local ifx  = prevFX + (fx - prevFX) * t
                            local ify  = prevFY + (fy - prevFY) * t
                            if inBounds(ifx, ify) then
                                local ia = prevAlpha + (alpha - prevAlpha) * t
                                local d  = GetMMDot()
                                d:SetSize(size, size)
                                d:SetPoint("CENTER", Minimap, "CENTER",
                                    ifx * mmSize, -ify * mmSize)
                                d.tex:SetVertexColor(c.r, c.g, c.b, ia)
                                mmActive[#mmActive + 1] = d
                            end
                        end
                    end
                end

                if inBounds(fx, fy) then
                    local d = GetMMDot()
                    d:SetSize(size, size)
                    d:SetPoint("CENTER", Minimap, "CENTER",
                        fx * mmSize, -fy * mmSize)
                    d.tex:SetVertexColor(c.r, c.g, c.b, alpha)
                    mmActive[#mmActive + 1] = d
                end

                prevPt    = pt
                prevFX    = fx
                prevFY    = fy
                prevAlpha = alpha
            else
                prevPt    = nil
                prevAlpha = nil
            end
        end
    end
end

-- ── Recording + Draw Ticker ────────────────────────────────────────────────

local RECORD_RATE = 0.10
local DRAW_RATE   = 0.05   -- ~20Hz redraw is plenty for a fading trail; avoids per-frame map calls
local recElapsed  = 0
local drawElapsed = 0

local function RecordOnUpdate(_, elapsed)
    local db = P.DB and P.DB.mapTrail
    if not db then return end
    local anyEnabled = db.enabled or db.minimapEnabled
    if not anyEnabled then
        RecycleList(mmActive, mmPool)
        return
    end

    if db.minimapEnabled then
        drawElapsed = drawElapsed + elapsed
        if drawElapsed >= DRAW_RATE then
            drawElapsed = 0
            DrawMinimap()
        end
    end

    recElapsed = recElapsed + elapsed
    if recElapsed < RECORD_RATE then return end
    recElapsed = 0

    -- Skip recording while world map is open to avoid map state conflicts
    if WorldMapFrame:IsVisible() then return end

    EnsureMapContext()
    local x, y = GetPlayerMapPosition("player")
    if not x or (x == 0 and y == 0) then
        mapContextDirty = true
        EnsureMapContext()
        x, y = GetPlayerMapPosition("player")
        if not x or (x == 0 and y == 0) then return end
    end
    local zoneID = GetCurrentMapAreaID() - 1

    local spSq = db.dotSpacing * 0.001
    spSq = spSq * spSq
    if not lastX or Dist2(x, y, lastX, lastY) >= spSq then
        lastX, lastY = x, y
        RecordPoint(x, y, zoneID)
    end
end

-- ── Exported API ───────────────────────────────────────────────────────────

function P.MapTrail_Toggle(_)
    local db = P.DB and P.DB.mapTrail
    if db and not db.minimapEnabled then RecycleList(mmActive, mmPool) end
end

function P.MapTrail_RefreshColors()
    local db = P.DB and P.DB.mapTrail
    if not db then return end
    local c = db.color
    for _, d in ipairs(mmActive) do
        local _,_,_,a = d.tex:GetVertexColor()
        d.tex:SetVertexColor(c.r, c.g, c.b, a)
    end
end

function P.MapTrail_RefreshSize()
    DrawMinimap()
end

local function ClearTrailBuffer()
    trailBuf = {}; trailHead = 0; trailCount = 0; trailZoneID = -1
    lastX, lastY = nil, nil
    RecycleList(mmActive, mmPool)
end

-- ============================================================
-- SECTION 2 — MINIMAP NODE FLASH
-- ============================================================

local Flash = { phase = false, elapsed = 0, ticker = nil }

local function FlashNorm()
    Flash.phase = false
    Minimap:SetBlipTexture(BLIP_NORM)
end

local function FlashGlow()
    Flash.phase = true
    Minimap:SetBlipTexture(BLIP_GLOW)
end

local function FlashShouldRun()
    local db = P.DB and P.DB.minimapFlash
    if not db or not db.enabled then return false end
    if db.sessionOnly and not P.sessionActive then return false end
    return true
end

local function FlashOnUpdate(_, elapsed)
    Flash.elapsed = Flash.elapsed + elapsed
    local db = P.DB and P.DB.minimapFlash
    if not db then return end
    local speed = db.flashSpeed or 0.4
    if Flash.elapsed < speed then return end
    Flash.elapsed = 0
    if not FlashShouldRun() then
        if Flash.phase then FlashNorm() end
        return
    end
    if Flash.phase then FlashNorm() else FlashGlow() end
end

function P.MinimapFlash_Toggle(enabled)
    if not enabled then FlashNorm() end
end

function P.MinimapFlash_Restart()
    Flash.elapsed = 0
end

-- ============================================================
-- SECTION 3 — INIT  (called from Core.lua OnEnable)
-- ============================================================

function RNPack:MapFeatures_Init()

    -- Carbonite compatibility: its custom map conflicts with SetMapToCurrentZone calls
    -- Disable player trail and node flash entirely when Carbonite is loaded
    local carboniteLoaded = IsAddOnLoaded("Carbonite") or _G.Nx or _G.CarboniteMap
    if carboniteLoaded then
        DEFAULT_CHAT_FRAME:AddMessage(
            "|cffFF6600[RealNibba.Pack]:|r Carbonite detected — Player Trail & Node Flash disabled to avoid map conflicts.")
        return
    end

    LMD = LibStub and LibStub("LibMapData-1.0", true)

    local function UpdateMMZone()
        local zoom = Minimap:GetZoom()
        if GetCVar("minimapZoom") == GetCVar("minimapInsideZoom") then
            Minimap:SetZoom(zoom > 0 and zoom - 1 or zoom + 1)
            mmIsInside = Minimap:GetZoom() == tonumber(GetCVar("minimapInsideZoom"))
            Minimap:SetZoom(zoom)
        else
            mmIsInside = zoom == tonumber(GetCVar("minimapInsideZoom"))
        end
    end
    UpdateMMZone()
    P:RegisterEvent("MINIMAP_UPDATE_ZOOM", UpdateMMZone)

    local recordFrame = CreateFrame("Frame", "RealNibbaPack_TrailRecord", UIParent)
    recordFrame:SetScript("OnUpdate", RecordOnUpdate)

    -- Map context can only go stale right after the world map was browsed to
    -- another zone and closed — refresh lazily at that single point instead
    -- of calling SetMapToCurrentZone() every tick.
    WorldMapFrame:HookScript("OnHide", function() mapContextDirty = true end)
    P:RegisterEvent("ZONE_CHANGED_NEW_AREA", function()
        mapContextDirty = true
        ClearTrailBuffer()
    end)

    Flash.ticker = CreateFrame("Frame", "RealNibbaPack_FlashTick", UIParent)
    Flash.ticker:SetScript("OnUpdate", FlashOnUpdate)

    local fdb = P.DB and P.DB.minimapFlash
    if not (fdb and fdb.enabled) then FlashNorm() end
end
