local addOnName, namespace = ...

local WTUI = namespace.WildThingsUI
local Theme = namespace.Theme

-- WTUI:AddBorder (square 4-strip border) lived here. Every caller now uses
-- WTUI:AddRoundedBorder below, which offers the same four methods -- SetBorderColor,
-- ShowBorder, HideBorder, SetBorderShown -- and follows the control's corner radius.

local CORNER_TEXTURE = [[Interface\CHARACTERFRAME\TempPortraitAlphaMaskSmall]]
-- Purpose-built rounded-RECT alpha mask (white RGB, alpha = coverage; 16x16 with fixed 4px
-- corner arcs and a SOLID centre/edges). Fed to the nine-slice mask in AddRoundedMask below.
-- TempPortraitAlphaMaskSmall (fine for the corner-quadrant *fill* above, where its 4 quadrants
-- are placed at a fixed size) is an oval/portrait mask whose solid region curves, so nine-slicing
-- + stretching IT produced pill/oval controls (Phase 29 UAT). A rounded-rect source with a solid
-- centre nine-slices into a fixed-4px-corner rectangle at any control width/height.
local ROUNDED_MASK_TEXTURE = string.format([[Interface\Addons\%s\UI\Media\Textures\RoundedRectMask.tga]], addOnName)
local CORNER_TEXCOORDS = {
    TopLeft     = {0, 0.5, 0, 0.5},
    TopRight    = {0.5, 1, 0, 0.5},
    BottomLeft  = {0, 0.5, 0.5, 1},
    BottomRight = {0.5, 1, 0.5, 1},
}

-- 128x128 rings whose four quadrants are the four corner ARCS of a rounded-rect outline
-- (same quadrant layout as CORNER_TEXCOORDS above). Deliberately far higher resolution than
-- the 4-8px they render at: sizing a corner texture independently of its source resolution
-- is what lets the GPU downsample high-res art with TRILINEAR filtering, which is the only
-- way a small-radius arc reads as a curve. Drawing the arc at its final size instead — what
-- SetTextureSliceMargins forces, since its margins are source texels — gives a 4x4-pixel arc
-- that is blurry with antialiasing and stair-stepped without. Both were built and rejected
-- in UAT. Other addons use this same
-- downsample technique; AddRoundedFill above has always used it for FILLS.
--
-- One variant per radius because SetSize scales the stroke along with the arc: each bakes a
-- 64/radius px source stroke so all of them land on exactly 1 screen px.
-- Regenerate with generate_corner_stroke.ps1 in the repo root; adding a radius here means
-- adding a matching variant there.
local CORNER_STROKE_TEXTURES = {
    [4] = string.format([[Interface\Addons\%s\UI\Media\Textures\RoundedCornerStroke_SM.tga]], addOnName),
    [6] = string.format([[Interface\Addons\%s\UI\Media\Textures\RoundedCornerStroke_MD.tga]], addOnName),
    [8] = string.format([[Interface\Addons\%s\UI\Media\Textures\RoundedCornerStroke_LG.tga]], addOnName),
}

-- Picks the stroke art baked for `radius`, falling back to the nearest available variant so
-- a caller passing an unlisted radius still gets a stroke (slightly off 1px) rather than nil.
local function GetCornerStrokeTexture(radius)
    local exact = CORNER_STROKE_TEXTURES[radius]
    if exact then return exact end

    local bestKey, bestDelta
    for key in pairs(CORNER_STROKE_TEXTURES) do
        local delta = math.abs(key - radius)
        if not bestDelta or delta < bestDelta then
            bestKey, bestDelta = key, delta
        end
    end
    return CORNER_STROKE_TEXTURES[bestKey]
end

-- Theme.RADIUS_PILL is a SENTINEL (999), not a measurement: it mirrors the design tokens'
-- `border-radius: 999px`, which a browser silently clamps to half the box. Every primitive
-- below takes `radius` in literal screen pixels -- corner art is drawn at radius x radius and
-- the straight runs are inset by radius at both ends -- so letting the sentinel through
-- unresolved paints ~1000px corner arcs anchored to the control and leaves the control itself
-- with no border on it at all. That shipped once: SummonScreen's warlock chip drew four
-- screen-sized accent-green arcs across the whole UI (999/8 = a ~125px stroke, because
-- GetCornerStrokeTexture falls back to the radius-8 art and SetSize scales its baked stroke).
--
-- Resolved against HEIGHT, not min(width, height): a pill in this design system is a
-- horizontal capsule, and controls are routinely built at a placeholder width and sized to
-- their content afterwards (the warlock chip is created 1px wide), so width is not yet
-- meaningful at build time while height always is.
local function ResolvePillRadius(frame, radius)
    if radius and radius >= Theme.RADIUS_PILL then
        return math.max(1, math.floor(((frame and frame:GetHeight()) or 0) / 2))
    end

    return radius
end

-- Builds a rounded 1px stroke as 4 downsampled corner arcs + 4 plain color edge strips,
-- parented to a container Frame so one SetAlpha/Show/Hide drives all 8 pieces. Only the
-- corners sample a texture; the straight runs are color textures and stay pixel-crisp
-- because nothing resamples them.
--
-- `corners` is an optional allowlist set (e.g. {TopLeft = true, BottomLeft = true}) matching
-- AddRoundedFill's 4th argument — an omitted corner renders square, which is what a control
-- split across two frames (Dropdown's body + arrow) needs so the seam between them stays flat.
--
-- `offsetX`/`offsetY` push the stroke OUTWARD from the frame edge (negative pulls it in),
-- with the same sign convention as AddBorder's horizontalOffset/verticalOffset. Needed by
-- EditBox's focus glow, which sits outside its frame, and by Slider, which pulls in by 3.
--
-- Returns the container, which carries :SetStrokeColor(r, g, b, a).
local function BuildRoundedStroke(frame, radius, thickness, corners, offsetX, offsetY)
    radius = ResolvePillRadius(frame, radius or Theme.RADIUS_SM)
    thickness = thickness or 1
    offsetX = offsetX or 0
    offsetY = offsetY or 0

    -- Per-corner anchor offsets, sign-flipped so a positive offset grows the stroke outward
    -- on every side.
    local cornerOffsets = {
        TopLeft     = {-offsetX,  offsetY},
        TopRight    = { offsetX,  offsetY},
        BottomLeft  = {-offsetX, -offsetY},
        BottomRight = { offsetX, -offsetY},
    }

    local container = CreateFrame("Frame", nil, frame)
    container:SetAllPoints(frame)
    -- Tracked as two explicit lists, NOT sniffed via GetTexture() at recolor time: a texture
    -- that has had SetColorTexture called on it also reports a texture, so sniffing would
    -- send the edges down the SetVertexColor path on every call after the first -- tinting
    -- them against the black they were initialised to, i.e. permanently black edges.
    container.arcs = {}   -- textured -> recolor with SetVertexColor
    container.fills = {}  -- blank    -> recolor with SetColorTexture

    local strokeTexture = GetCornerStrokeTexture(radius)

    for cornerName, texcoord in pairs(CORNER_TEXCOORDS) do
        local rounded = (not corners) or corners[cornerName]
        local co = cornerOffsets[cornerName]
        local piece = container:CreateTexture(nil, "OVERLAY")
        piece:SetPoint(cornerName, frame, cornerName, co[1], co[2])

        if rounded then
            -- TRILINEAR is the load-bearing argument: it is what mip-filters the 128px arc
            -- down to `radius` px smoothly. Details! passes the same three arguments.
            piece:SetSize(radius, radius)
            piece:SetTexture(strokeTexture, "CLAMP", "CLAMP", "TRILINEAR")
            piece:SetTexCoord(unpack(texcoord))
            table.insert(container.arcs, piece)
        else
            -- Square corner: an L of two thin strips filling the same radius x radius slot,
            -- so the edge runs below (which always inset by `radius`) still meet something.
            piece:SetSize(radius, thickness)
            local vertical = container:CreateTexture(nil, "OVERLAY")
            vertical:SetPoint(cornerName, frame, cornerName, co[1], co[2])
            vertical:SetSize(thickness, radius)
            vertical:SetSnapToPixelGrid(false)
            vertical:SetTexelSnappingBias(0)
            table.insert(container.fills, vertical)
            table.insert(container.fills, piece)
        end

        piece:SetSnapToPixelGrid(false)
        piece:SetTexelSnappingBias(0)
    end

    -- Straight runs, inset by `radius` at both ends so they meet the arcs' tangent points,
    -- and carrying the same outward offset as the corners they join.
    local edges = {
        top    = {"TOPLEFT", -offsetX + radius, offsetY, "TOPRIGHT", offsetX - radius, offsetY},
        bottom = {"BOTTOMLEFT", -offsetX + radius, -offsetY, "BOTTOMRIGHT", offsetX - radius, -offsetY},
        left   = {"TOPLEFT", -offsetX, offsetY - radius, "BOTTOMLEFT", -offsetX, -offsetY + radius},
        right  = {"TOPRIGHT", offsetX, offsetY - radius, "BOTTOMRIGHT", offsetX, -offsetY + radius},
    }
    for edgeName, a in pairs(edges) do
        local edge = container:CreateTexture(nil, "OVERLAY")
        edge:SetPoint(a[1], frame, a[1], a[2], a[3])
        edge:SetPoint(a[4], frame, a[4], a[5], a[6])
        if edgeName == "top" or edgeName == "bottom" then
            edge:SetHeight(thickness)
        else
            edge:SetWidth(thickness)
        end
        edge:SetSnapToPixelGrid(false)
        edge:SetTexelSnappingBias(0)
        table.insert(container.fills, edge)
    end

    function container:SetStrokeColor(r, g, b, a)
        if not a then a = 1 end
        for _, arc in ipairs(container.arcs) do
            arc:SetVertexColor(r, g, b, a)
        end
        for _, fill in ipairs(container.fills) do
            fill:SetColorTexture(r, g, b, a)
        end
    end

    container:SetStrokeColor(0, 0, 0)

    return container
end

-- Public wrapper around the primitive above, for callers that want a standalone stroke they
-- drive themselves (Show/Hide, SetAlpha, or WTUI:AddHoverFade with opts.alphaOnly).
function WTUI:AddRoundedStroke(frame, radius, thickness, corners, offsetX, offsetY)
    frame.roundedStroke = BuildRoundedStroke(frame, radius, thickness, corners, offsetX, offsetY)
    return frame.roundedStroke
end

-- Adds a rounded-corner fill to a frame (corner-quadrant technique, spike-001 VALIDATED)
-- radius is a fixed pixel value (pass Theme.RADIUS_SM/MD/LG); color is an optional {r, g, b, a} Theme table
-- corners is an optional allowlist set (e.g. {TopLeft = true, TopRight = true}) selecting which
-- corners render rounded; nil/omitted = all four rounded (Phase-27 callers -- Window.lua:28,
-- Tooltip.lua:28 -- pass no 4th arg, so behavior is byte-for-byte unchanged for them).
function WTUI:AddRoundedFill(frame, radius, color, corners)
    radius = ResolvePillRadius(frame, radius)
    frame.roundedFill = {}

    -- 4 corners
    for cornerName, texcoord in pairs(CORNER_TEXCOORDS) do
        local tex = frame:CreateTexture(nil, "BACKGROUND")

        if not corners or corners[cornerName] then
            -- Rounded: alpha-masked quadrant (unchanged from today). CLAMP prevents
            -- bilinear filtering from bleeding across the texcoord quadrant seam at small radii.
            tex:SetTexture(CORNER_TEXTURE, "CLAMP", "CLAMP", "TRILINEAR")
            tex:SetTexCoord(unpack(texcoord))
        else
            -- Square: same radius x radius slot, same corner anchor -- but a solid base
            -- (identical to the edge/center infill base below) instead of the mask, so this
            -- corner is fully opaque flush to the frame's true corner. topEdge/bottomEdge/
            -- centerBlock already anchor to this texture's inner-facing sides (unchanged), so
            -- the square corner tiles flush against them with no seam and no gap.
            tex:SetColorTexture(1, 1, 1, 1)
        end

        tex:SetSize(radius, radius)
        tex:SetPoint(cornerName, frame, cornerName, 0, 0)
        tex:SetSnapToPixelGrid(false)
        tex:SetTexelSnappingBias(0)
        frame.roundedFill[cornerName] = tex
    end

    -- top edge: stretches between TopLeft's right side and TopRight's left side.
    -- Infill blocks need a solid base so SetVertexColor has something to tint -- without a
    -- SetColorTexture they draw nothing and the panel body renders transparent.
    local topEdge = frame:CreateTexture(nil, "BACKGROUND")
    topEdge:SetColorTexture(1, 1, 1, 1)
    topEdge:SetPoint("TOPLEFT", frame.roundedFill.TopLeft, "TOPRIGHT")
    topEdge:SetPoint("BOTTOMLEFT", frame.roundedFill.TopLeft, "BOTTOMRIGHT")
    topEdge:SetPoint("TOPRIGHT", frame.roundedFill.TopRight, "TOPLEFT")
    topEdge:SetPoint("BOTTOMRIGHT", frame.roundedFill.TopRight, "BOTTOMLEFT")
    topEdge:SetSnapToPixelGrid(false)
    topEdge:SetTexelSnappingBias(0)
    frame.roundedFill.TopEdge = topEdge

    -- bottom edge: mirror, between BottomLeft/BottomRight
    local bottomEdge = frame:CreateTexture(nil, "BACKGROUND")
    bottomEdge:SetColorTexture(1, 1, 1, 1)
    bottomEdge:SetPoint("TOPLEFT", frame.roundedFill.BottomLeft, "TOPRIGHT")
    bottomEdge:SetPoint("BOTTOMLEFT", frame.roundedFill.BottomLeft, "BOTTOMRIGHT")
    bottomEdge:SetPoint("TOPRIGHT", frame.roundedFill.BottomRight, "TOPLEFT")
    bottomEdge:SetPoint("BOTTOMRIGHT", frame.roundedFill.BottomRight, "BOTTOMLEFT")
    bottomEdge:SetSnapToPixelGrid(false)
    bottomEdge:SetTexelSnappingBias(0)
    frame.roundedFill.BottomEdge = bottomEdge

    -- center block: fills the vertical middle between the left corners and right corners.
    -- NOTE: the two right-side anchors deliberately target different corners
    -- (TopRight/BottomRight) -- fixes the Details! Framework rounded_panel.lua
    -- copy-paste bug where both right anchors pointed at BottomRight's "topright".
    local centerBlock = frame:CreateTexture(nil, "BACKGROUND")
    centerBlock:SetColorTexture(1, 1, 1, 1)
    centerBlock:SetPoint("TOPLEFT", frame.roundedFill.TopLeft, "BOTTOMLEFT")
    centerBlock:SetPoint("BOTTOMLEFT", frame.roundedFill.BottomLeft, "TOPLEFT")
    centerBlock:SetPoint("TOPRIGHT", frame.roundedFill.TopRight, "BOTTOMRIGHT")
    centerBlock:SetPoint("BOTTOMRIGHT", frame.roundedFill.BottomRight, "TOPRIGHT")
    centerBlock:SetSnapToPixelGrid(false)
    centerBlock:SetTexelSnappingBias(0)
    frame.roundedFill.CenterBlock = centerBlock

    function frame:SetRoundedFillColor(r, g, b, a)
        if not a then a = 1 end
        for _, tex in pairs(frame.roundedFill) do
            tex:SetVertexColor(r, g, b, a)
        end
    end

    if color then
        frame:SetRoundedFillColor(color.r, color.g, color.b, color.a)
    else
        frame:SetRoundedFillColor(1, 1, 1, 1)
    end
end

-- Adds a reusable rounded-corner MASK to a frame (nine-slice technique, spike-002).
-- radius is a fixed pixel value (Theme.RADIUS_SM/MD/LG). The SAME mask instance can be
-- attached to any number of the frame's own full-rect textures via AddMaskTexture --
-- clipping the base fill and every overlay/wash layer to one shared rounded shape.
--
-- Deliberately NOT run through ResolvePillRadius, unlike the stroke and fill primitives:
-- SetTextureSliceMargins takes SOURCE TEXELS, and ROUNDED_MASK_TEXTURE is 16x16, so radius
-- must stay <= 8 here no matter how large the control is. Theme.RADIUS_PILL is therefore
-- unsupported on this primitive -- pass RADIUS_LG (8), the source's half-size, for the
-- roundest mask the art can express. Every caller today passes RADIUS_SM or RADIUS_MD.
function WTUI:AddRoundedMask(frame, radius)
    local mask = frame:CreateMaskTexture(nil, "BACKGROUND")

    -- ROUNDED_MASK_TEXTURE is a purpose-built rounded-RECT alpha mask (see its definition
    -- above) -- NOT TempPortraitAlphaMaskSmall, whose curved solid region nine-sliced into
    -- pill/oval controls (Phase 29 UAT fix). CLAMPTOBLACKADDITIVE is the standard MaskTexture
    -- wrap mode: areas the mask source doesn't cover clamp to fully-transparent (0).
    mask:SetTexture(ROUNDED_MASK_TEXTURE, "CLAMPTOBLACKADDITIVE", "CLAMPTOBLACKADDITIVE")

    -- Patch 10.2.0 API (TextureBase, shared by Texture AND MaskTexture): holds the
    -- corner region at a fixed `radius` px regardless of the mask's rendered width/height
    -- -- this is the built-in engine fix for a naive-stretch mask's ovalizing pitfall.
    mask:SetTextureSliceMargins(radius, radius, radius, radius)
    mask:SetTextureSliceMode(Enum.UITextureSliceMode.Stretched)

    -- SetAllPoints (not a fixed size): the mask auto-resizes with the control frame, so
    -- resizing a button/slider/progress bar never needs a second AddRoundedMask call.
    mask:SetAllPoints(frame)
    mask:SetSnapToPixelGrid(false)
    mask:SetTexelSnappingBias(0)

    frame.roundedMask = mask
    return mask
end

-- Adds a rounded-corner BORDER to a frame. This is the only border primitive: a straight
-- 4-strip border cannot round its own corners, and masking those strips would leave the
-- corner arcs empty.
--
-- Technique: BuildRoundedStroke's 4 downsampled corner arcs + 4 crisp color edges, drawn
-- ON TOP of the frame's own layers. This REPLACED an earlier implementation that put a
-- masked full-rect texture BELOW the base fill and let the color show through a 1px-inset
-- fill: that only works when the fill is OPAQUE. Against a translucent fill (Button's
-- BTN_BG is a = 0.6) the border bled across the whole face at ~40% instead of reading as
-- an edge -- confirmed in-game.
--
-- The old under-the-fill approach required callers to inset their base fill by `thickness`.
-- That inset is now harmless rather than required (the stroke covers exactly that band), so
-- existing callers -- Pill.lua:36-37, EditBox, BankScreen, GroupGrid, RaidTimelineScreen --
-- keep working untouched. The four border methods below are unchanged in name and meaning.
--
-- radius defaults to Theme.RADIUS_SM, thickness to 1. `corners` is an optional allowlist set
-- (see BuildRoundedStroke). Returns the stroke container, NOT a texture as it used to --
-- no caller used the old return value.
function WTUI:AddRoundedBorder(frame, radius, thickness, corners, offsetX, offsetY)
    frame.roundedBorder = BuildRoundedStroke(frame, radius, thickness, corners, offsetX, offsetY)

    function frame:SetBorderColor(r, g, b, a)
        frame.roundedBorder:SetStrokeColor(r, g, b, a)
    end

    function frame:ShowBorder()
        frame.roundedBorder:Show()
    end

    function frame:HideBorder()
        frame.roundedBorder:Hide()
    end

    function frame:SetBorderShown(shown)
        if shown then
            frame:ShowBorder()
        else
            frame:HideBorder()
        end
    end

    frame:SetBorderColor(0, 0, 0)

    return frame.roundedBorder
end

-- Adds a hover outline to a frame, faded in while the cursor is over it.
-- If an alt frame is provided, the outline is drawn on the alt frame but driven by hovering
-- the main frame (Dropdown's body highlights its detached arrow half this way).
--
-- REBUILT on BuildRoundedStroke. The previous implementation drew four 1px HIGHLIGHT-layer
-- strips, which had three problems, all confirmed in-game:
--   1. Square. It cut straight across the 4px corner arcs of every rounded control.
--   2. Left/right were never given a width (only SetHeight was called on all four), so the
--      "outline" was in practice just a top and bottom hairline.
--   3. HIGHLIGHT is engine-driven, so it snapped on/off against the 0.12s/0.20s hover fades
--      running on the same controls.
-- Now: rounded, all four sides real, and fading on the shared AddHoverFade timing.
--
-- Signature is preserved so existing callers need no change. `width` maps to stroke
-- thickness; `radius`/`corners` are new optional trailing arguments.
function WTUI:AddHoverHighlight(frame, altFrame, width, r, g, b, a, radius, corners)
    if not altFrame then altFrame = frame end

    -- Create-once (ROADMAP SC5): CheckButton.lua:145,154 re-calls this on state changes to
    -- recolor. Rebuilding would stack orphaned strokes fighting over the same alpha.
    if not frame.highlight then
        frame.highlight = BuildRoundedStroke(altFrame, radius or Theme.RADIUS_SM, width or 1, corners)
        WTUI:AddHoverFade(frame, frame.highlight, {alphaOnly = true})
    end

    frame.highlight:SetStrokeColor(
        r or Theme.HOVER_OVERLAY.r,
        g or Theme.HOVER_OVERLAY.g,
        b or Theme.HOVER_OVERLAY.b,
        a or Theme.HOVER_OVERLAY.a
    )
end

-- Adds a tooltip to a frame
-- Can be called repeatedly to change the tooltip
function WTUI:AddTooltip(frame, tooltipText, secondaryTooltipText)
    if not tooltipText then tooltipText = "" end

    frame.secondaryTooltipText = secondaryTooltipText -- Used for stuff like warnings/additional info that shouldn't change the main tooltip text

    -- If this frame already has a tooltip applied to it, simply change the tooltip text
    if frame.tooltipText then
        frame.tooltipText = tooltipText
    else
        frame.tooltipText = tooltipText

        -- The tooltip should be handled in a hook, in case the OnEnter/OnLeave script changes later on
        -- If there is no OnEnter/OnLeave script present, add an empty one
        if not frame:HasScript("OnEnter") then
            frame:SetScript("OnEnter", function() end)
        end

        if not frame:HasScript("OnLeave") then
            frame:SetScript("OnLeave", function() end)
        end

        frame:HookScript(
            "OnEnter",
            function()
                if not frame.tooltipText or frame.tooltipText == "" then return end

                WTUI.Tooltip:Hide()
                WTUI.Tooltip:SetOwner(frame, "ANCHOR_RIGHT")

                WTUI.Tooltip:SetText(frame.tooltipText, Theme.TEXT.r, Theme.TEXT.g, Theme.TEXT.b, 1, true)

                if frame.secondaryTooltipText and frame.secondaryTooltipText ~= "" then
                    WTUI.Tooltip:AddLine(frame.secondaryTooltipText, Theme.TEXT_DIM.r, Theme.TEXT_DIM.g, Theme.TEXT_DIM.b, true)
                end

                WTUI.Tooltip:Show()
            end
        )

        frame:HookScript(
            "OnLeave",
            function()
                WTUI.Tooltip:Hide()
            end
        )
    end
end

-- Refreshes the tooltip that is currently showing
-- Useful mainly for when editbox vlaues in reminder config are changed, and tooltip warnings are added/hidden as a result
function WTUI:RefreshTooltip()
    if WTUI.Tooltip:IsVisible() then
        local frame = WTUI.Tooltip:GetOwner()

        if frame and frame.tooltipText then
            WTUI.Tooltip:SetText(frame.tooltipText, Theme.TEXT.r, Theme.TEXT.g, Theme.TEXT.b, 1, true)

            if frame.secondaryTooltipText and frame.secondaryTooltipText ~= "" then
                WTUI.Tooltip:AddLine(frame.secondaryTooltipText, Theme.TEXT_DIM.r, Theme.TEXT_DIM.g, Theme.TEXT_DIM.b, true)
            end

            WTUI.Tooltip:Show()
        else
            WTUI.Tooltip:Hide()
        end
    end
end

-- Save the size/position of a frame in SavedVariables, keyed by some name
function WTUI:SaveSize(frame, name)
    if not name then return end

    if not WTUI.frameSettings[name] then
        WTUI.frameSettings[name] = {}
    end

    local width, height = frame:GetSize()

    WTUI.frameSettings[name].width = width
    WTUI.frameSettings[name].height = height
end

function WTUI:SavePosition(frame, name)
    if not name then return end

    if not WTUI.frameSettings[name] then
        WTUI.frameSettings[name] = {}
    end

    WTUI.frameSettings[name].points = {}

    local numPoints = frame:GetNumPoints()

    for i = 1, numPoints do
        local point, relativeTo, relativePoint, offsetX, offsetY = frame:GetPoint(i)

        if relativeTo == nil or relativeTo == UIParent then -- Only consider points relative to UIParent
            table.insert(
                WTUI.frameSettings[name].points,
                {
                    point = point,
                    relativePoint = relativePoint,
                    offsetX = offsetX,
                    offsetY = offsetY
                }
            )
        end
    end
end

-- Restore and apply saved size/position to a frame, keyed by some name
-- Returns whether this was successful, so the user can apply some default values if not
function WTUI:RestoreSize(frame, name)
    if not name then return false end

    local settings = WTUI.frameSettings[name]

    if not settings then return false end
    if not settings.width then return false end
    if not settings.height then return false end

    frame:SetSize(settings.width, settings.height)

    return true
end

function WTUI:RestorePosition(frame, name)
    if not name then return false end

    local points = name and WTUI.frameSettings[name] and WTUI.frameSettings[name].points

    if not points then return false end

    for _, pointInfo in ipairs(points) do
        frame:SetPoint(pointInfo.point, UIParent, pointInfo.relativePoint, pointInfo.offsetX, pointInfo.offsetY)
    end

    return true
end

-- Exposed for BindWidget.lua's DirtyFlag decorator, which needs its own BuildRoundedStroke
-- instance (stored as frame._dirtyBorder) independent of frame.roundedBorder.
WTUI.BuildRoundedStroke = BuildRoundedStroke
