-- ============================================================================
-- pom_stack_settings.lua
-- Experimental visual stacking test for POM
--
-- IMPORTANT:
-- - Self-contained test file
-- - Does NOT require changes in pom.lua
-- - Does NOT call SetCVar / ConsoleExec
-- - Does NOT move the Blizzard nameplate frame itself
-- - Tries to move POM-owned visual elements only
--
-- Slash:
--   /pomst
-- ============================================================================

local ADDON_NAME, ns = ...

ns = _G[ADDON_NAME] or ns or {}
_G[ADDON_NAME] = ns

ns.stacktest = ns.stacktest or {}
local M = ns.stacktest

local sort = table.sort
local tonumber = tonumber
local type = type
local pcall = pcall
local math_floor = math.floor
local UnitIsUnit = UnitIsUnit
local UnitExists = UnitExists
local CreateFrame = CreateFrame
local C_NamePlate = C_NamePlate
local hooksecurefunc = hooksecurefunc
local C_Timer = C_Timer

-- ----------------------------------------------------------
-- DB
-- ----------------------------------------------------------
local DEFAULTS = {
    enabled = false,
    spacing = 14,
    debug = false,
}

local function GetRootDB()
    _G.PlateOMaticDB = _G.PlateOMaticDB or {}
    return _G.PlateOMaticDB
end

local function GetDB()
    local root = GetRootDB()
    root.stacking = root.stacking or {}

    if root.stacking.enabled == nil then
        root.stacking.enabled = DEFAULTS.enabled
    end
    if root.stacking.spacing == nil then
        root.stacking.spacing = DEFAULTS.spacing
    end
    if root.stacking.debug == nil then
        root.stacking.debug = DEFAULTS.debug
    end

    return root.stacking
end

local function ClampSpacing(v)
    v = tonumber(v) or DEFAULTS.spacing
    if v < 0 then return 0 end
    if v > 60 then return 60 end
    return math_floor(v + 0.5)
end

local function Dbg(...)
    local db = GetDB()
    if not db.debug then return end
    print("|cFF33FF99POM StackTest:|r", ...)
end

-- ----------------------------------------------------------
-- Storage
-- ----------------------------------------------------------
local touchedFrames = setmetatable({}, { __mode = "k" })
local trackedPlates = setmetatable({}, { __mode = "k" })

-- ----------------------------------------------------------
-- Helpers
-- ----------------------------------------------------------
local function SafeClearAllPoints(obj)
    if not obj or (obj.IsForbidden and obj:IsForbidden()) then return false end
    if not obj.ClearAllPoints then return false end
    return pcall(obj.ClearAllPoints, obj)
end

local function SafeSetPoint(obj, ...)
    if not obj or (obj.IsForbidden and obj:IsForbidden()) then return false end
    if not obj.SetPoint then return false end
    return pcall(obj.SetPoint, obj, ...)
end

local function SafeGetPoint(obj, idx)
    if not obj or (obj.IsForbidden and obj:IsForbidden()) then return nil end
    if not obj.GetPoint then return nil end
    local ok, a, b, c, d, e = pcall(obj.GetPoint, obj, idx or 1)
    if not ok then return nil end
    return a, b, c, d, e
end

local function SafeGetNumPoints(obj)
    if not obj or (obj.IsForbidden and obj:IsForbidden()) then return 0 end
    if not obj.GetNumPoints then return 0 end
    local ok, n = pcall(obj.GetNumPoints, obj)
    if not ok then return 0 end
    return tonumber(n) or 0
end

local function SaveOriginalPoint(obj)
    if not obj then return end
    if touchedFrames[obj] then return end

    local point, relativeTo, relativePoint, xOfs, yOfs = SafeGetPoint(obj, 1)
    if not point then return end

    touchedFrames[obj] = {
        point = point,
        relativeTo = relativeTo,
        relativePoint = relativePoint,
        xOfs = xOfs or 0,
        yOfs = yOfs or 0,
    }
end

local function RestoreOriginalPoint(obj)
    local saved = touchedFrames[obj]
    if not saved then return end

    SafeClearAllPoints(obj)
    SafeSetPoint(
        obj,
        saved.point,
        saved.relativeTo,
        saved.relativePoint,
        saved.xOfs,
        saved.yOfs
    )
end

local function IsShownSafe(obj)
    if not obj or (obj.IsForbidden and obj:IsForbidden()) then return false end
    if not obj.IsShown then return false end
    local ok, shown = pcall(obj.IsShown, obj)
    return ok and shown and true or false
end

local function AddCandidate(list, obj)
    if not obj then return end
    if obj.IsForbidden and obj:IsForbidden() then return end
    list[#list + 1] = obj
end

local function CollectVisualCandidates(frame)
    local out = {}
    if not frame then return out end

    local mixin = frame.POM_Mixin

    if mixin then
        AddCandidate(out, mixin.healthBar)
        AddCandidate(out, mixin.nameText)
        AddCandidate(out, mixin.levelText)
        AddCandidate(out, mixin.healthText)
        AddCandidate(out, mixin.percentText)
        AddCandidate(out, mixin.questIcon)
        AddCandidate(out, mixin.questText)
        AddCandidate(out, mixin.raidIcon)
        AddCandidate(out, mixin.classIcon)
        AddCandidate(out, mixin.targetArrow)
        AddCandidate(out, mixin.topAnchor)
        AddCandidate(out, mixin.bottomAnchor)
        AddCandidate(out, mixin.castBar)
        AddCandidate(out, mixin.customCastbar)
        AddCandidate(out, mixin.border)
        AddCandidate(out, mixin.bg)
        AddCandidate(out, mixin.background)
        AddCandidate(out, mixin.overlay)
        AddCandidate(out, mixin.highlight)
    end

    -- Fallbacks on frame itself in case POM attached there
    AddCandidate(out, frame.healthBar)
    AddCandidate(out, frame.name)
    AddCandidate(out, frame.nameText)
    AddCandidate(out, frame.levelText)
    AddCandidate(out, frame.healthText)
    AddCandidate(out, frame.BuffFrame)
    AddCandidate(out, frame.DebuffFrame)
    AddCandidate(out, frame.castBar)

    -- Deduplicate
    local unique, final = {}, {}
    for i = 1, #out do
        local obj = out[i]
        if obj and not unique[obj] then
            unique[obj] = true
            final[#final + 1] = obj
        end
    end

    return final
end

local function ApplyOffsetToVisuals(frame, offsetY)
    if not frame then return end
    local candidates = CollectVisualCandidates(frame)

    for i = 1, #candidates do
        local obj = candidates[i]
        local numPoints = SafeGetNumPoints(obj)
        if numPoints > 0 then
            SaveOriginalPoint(obj)
            local saved = touchedFrames[obj]
            if saved then
                SafeClearAllPoints(obj)
                SafeSetPoint(
                    obj,
                    saved.point,
                    saved.relativeTo,
                    saved.relativePoint,
                    saved.xOfs,
                    (saved.yOfs or 0) + offsetY
                )
            end
        end
    end

    frame.__pomStackOffsetY = offsetY
end

local function RestoreFrameVisuals(frame)
    if not frame then return end
    local candidates = CollectVisualCandidates(frame)
    for i = 1, #candidates do
        RestoreOriginalPoint(candidates[i])
    end
    frame.__pomStackOffsetY = nil
end

local function CollectActiveFrames()
    local result = {}
    local plates = C_NamePlate.GetNamePlates and C_NamePlate.GetNamePlates() or nil
    if not plates then return result end

    for i = 1, #plates do
        local plate = plates[i]
        local uf = plate and plate.UnitFrame
        if uf and uf.unit and IsShownSafe(uf) then
            uf.__pomStackOrder = i
            result[#result + 1] = uf
            trackedPlates[uf] = true
        end
    end

    return result
end

local function SortFrames(list)
    sort(list, function(a, b)
        local au = a and a.unit
        local bu = b and b.unit

        local aTarget = au and UnitExists("target") and UnitIsUnit(au, "target") or false
        local bTarget = bu and UnitExists("target") and UnitIsUnit(bu, "target") or false

        if aTarget ~= bTarget then
            return aTarget
        end

        local ao = (a and a.__pomStackOrder) or 0
        local bo = (b and b.__pomStackOrder) or 0
        return ao < bo
    end)
end

-- ----------------------------------------------------------
-- Core
-- ----------------------------------------------------------
function M:ClearAll()
    for frame in pairs(trackedPlates) do
        RestoreFrameVisuals(frame)
    end
end

function M:ApplyNow()
    local db = GetDB()

    if not db.enabled then
        self:ClearAll()
        return
    end

    local spacing = ClampSpacing(db.spacing)
    local frames = CollectActiveFrames()
    if #frames == 0 then return end

    SortFrames(frames)

    for i = 1, #frames do
        local frame = frames[i]
        local offsetY = -((i - 1) * spacing)
        ApplyOffsetToVisuals(frame, offsetY)
    end

    Dbg("Applied stacking. count =", #frames, "spacing =", spacing)
end

function M:Refresh()
    self:ApplyNow()
end

-- ----------------------------------------------------------
-- Hook strategy
-- ----------------------------------------------------------
local function TryApplyToFrame(frame)
    if not frame then return end

    local db = GetDB()
    if not db.enabled then
        RestoreFrameVisuals(frame)
        return
    end

    M:ApplyNow()
end

local function InstallHooks()
    if M.__hooksInstalled then return end
    M.__hooksInstalled = true

    hooksecurefunc("CompactUnitFrame_UpdateAll", function(frame)
        TryApplyToFrame(frame)
    end)

    hooksecurefunc("CompactUnitFrame_UpdateHealth", function(frame)
        TryApplyToFrame(frame)
    end)

    hooksecurefunc("CompactUnitFrame_UpdateName", function(frame)
        TryApplyToFrame(frame)
    end)
end

-- ----------------------------------------------------------
-- UI helpers
-- ----------------------------------------------------------
local function CreateLabel(parent, text, x, y, template)
    local fs = parent:CreateFontString(nil, "OVERLAY", template or "GameFontNormal")
    fs:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y)
    fs:SetJustifyH("LEFT")
    fs:SetText(text or "")
    return fs
end

local function CreateCheckbox(parent, text, x, y, onClick)
    local b = CreateFrame("CheckButton", nil, parent, "UICheckButtonTemplate")
    b:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y)

    local fs = b.Text
    if fs then
        fs:SetText(text or "")
        fs:SetTextColor(1, 1, 1)
    end

    b:SetScript("OnClick", function(self)
        if onClick then
            onClick(self:GetChecked() and true or false)
        end
    end)

    return b
end

local function CreateSlider(parent, title, minVal, maxVal, step, x, y, width, getter, setter, formatter)
    local s = CreateFrame("Slider", nil, parent, "OptionsSliderTemplate")
    s:SetPoint("TOPLEFT", parent, "TOPLEFT", x, y)
    s:SetWidth(width or 260)
    s:SetMinMaxValues(minVal, maxVal)
    s:SetValueStep(step)
    s:SetObeyStepOnDrag(true)

    if s.Text then s.Text:SetText(title or "") end
    if s.Low then s.Low:SetText(tostring(minVal)) end
    if s.High then s.High:SetText(tostring(maxVal)) end

    s.ValueText = parent:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    s.ValueText:SetPoint("TOP", s, "BOTTOM", 0, 0)

    local function UpdateText()
        local v = s:GetValue()
        if formatter then
            s.ValueText:SetText(formatter(v))
        else
            s.ValueText:SetText(tostring(v))
        end
    end

    s:SetScript("OnValueChanged", function(_, value)
        if setter then setter(value) end
        UpdateText()
    end)

    function s:Refresh()
        if getter then
            self:SetValue(getter())
        end
        UpdateText()
    end

    return s
end

-- ----------------------------------------------------------
-- UI
-- ----------------------------------------------------------
function M:Build()
    if self.frame then return end

    local f = CreateFrame("Frame", "POM_StackTestFrame", UIParent, "BasicFrameTemplateWithInset")
    f:SetSize(460, 270)
    f:SetPoint("CENTER")
    f:SetMovable(true)
    f:EnableMouse(true)
    f:RegisterForDrag("LeftButton")
    f:SetScript("OnDragStart", f.StartMoving)
    f:SetScript("OnDragStop", f.StopMovingOrSizing)
    f:Hide()

    if f.TitleText then
        f.TitleText:SetText("POM Stacking Test")
    end

    CreateLabel(f, "Experimental visual offset test.", 16, -36, "GameFontNormal")
    CreateLabel(f, "Moves POM-owned visuals only, not the Blizzard plate itself.", 16, -54, "GameFontHighlightSmall")

    local enabledCB = CreateCheckbox(f, "Enable visual stacking test", 16, -84, function(checked)
        local db = GetDB()
        db.enabled = checked and true or false
        M:ApplyNow()
    end)

    local debugCB = CreateCheckbox(f, "Debug chat prints", 16, -108, function(checked)
        local db = GetDB()
        db.debug = checked and true or false
    end)

    local spacingSlider = CreateSlider(
        f,
        "Spacing",
        0, 60, 1,
        20, -156, 300,
        function()
            return ClampSpacing(GetDB().spacing)
        end,
        function(value)
            local db = GetDB()
            db.spacing = ClampSpacing(value)
            M:ApplyNow()
        end,
        function(v)
            return string.format("Current: %d", math_floor(v + 0.5))
        end
    )

    local applyBtn = CreateFrame("Button", nil, f, "UIPanelButtonTemplate")
    applyBtn:SetSize(120, 24)
    applyBtn:SetPoint("BOTTOMLEFT", f, "BOTTOMLEFT", 16, 16)
    applyBtn:SetText("Apply")
    applyBtn:SetScript("OnClick", function()
        M:ApplyNow()
    end)

    local clearBtn = CreateFrame("Button", nil, f, "UIPanelButtonTemplate")
    clearBtn:SetSize(120, 24)
    clearBtn:SetPoint("BOTTOM", f, "BOTTOM", 0, 16)
    clearBtn:SetText("Clear")
    clearBtn:SetScript("OnClick", function()
        M:ClearAll()
    end)

    local closeBtn = CreateFrame("Button", nil, f, "UIPanelButtonTemplate")
    closeBtn:SetSize(120, 24)
    closeBtn:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -16, 16)
    closeBtn:SetText("Close")
    closeBtn:SetScript("OnClick", function()
        f:Hide()
    end)

    function f:Refresh()
        local db = GetDB()
        enabledCB:SetChecked(db.enabled and true or false)
        debugCB:SetChecked(db.debug and true or false)
        spacingSlider:Refresh()
    end

    self.frame = f
end

function M:Toggle()
    if not self.frame then
        self:Build()
    end

    if self.frame:IsShown() then
        self.frame:Hide()
    else
        self.frame:Refresh()
        self.frame:Show()
    end
end

-- ----------------------------------------------------------
-- Events
-- ----------------------------------------------------------
local ev = CreateFrame("Frame")
ev:RegisterEvent("PLAYER_LOGIN")
ev:RegisterEvent("NAME_PLATE_UNIT_ADDED")
ev:RegisterEvent("NAME_PLATE_UNIT_REMOVED")
ev:RegisterEvent("PLAYER_TARGET_CHANGED")
ev:SetScript("OnEvent", function(_, event)
    if event == "PLAYER_LOGIN" then
        GetDB()
        InstallHooks()
        C_Timer.After(1, function()
            M:ApplyNow()
        end)
    elseif event == "NAME_PLATE_UNIT_ADDED" then
        M:ApplyNow()
    elseif event == "NAME_PLATE_UNIT_REMOVED" then
        M:ApplyNow()
    elseif event == "PLAYER_TARGET_CHANGED" then
        M:ApplyNow()
    end
end)

-- ----------------------------------------------------------
-- Slash
-- IMPORTANT:
-- remove /pomst from pom.lua if you added it there
-- ----------------------------------------------------------
SLASH_POMSTACKTEST1 = "/pomst"
SlashCmdList["POMSTACKTEST"] = function()
    if ns.stacktest and ns.stacktest.Toggle then
        ns.stacktest:Toggle()
    end
end