local _, namespace = ...

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

local DEFAULT_WIDTH = 200
local DEFAULT_HEIGHT = 20

function namespace:CreateSlider(parent, title, min, max, OnValueChanged, initialValue)
    local slider = CreateFrame("Slider", nil, parent, "UISliderTemplateWithLabels")

    -- dragging: true while the mouse button is held on the thumb (per-pixel OnValueChanged
    -- ticks are visual-only during this phase). initialized: false while a programmatic
    -- SetValue (construction, deferred first-sync, or the Task-2 inline EditBox commit) is
    -- bracketing the native SetValue call, so its synchronous OnValueChanged fire never
    -- re-triggers the caller's set.
    local dragging = false
    local initialized = false

    slider.OnEnter = function() end
    slider.OnLeave = function() end

    slider:SetScript("OnEnter", function(_self) _self.OnEnter() end)
    slider:SetScript("OnLeave", function(_self) _self.OnLeave() end)

    slider:SetSize(DEFAULT_WIDTH, DEFAULT_HEIGHT)
    slider:SetMinMaxValues(min, max)
    slider:SetValueStep(1)
    slider:SetObeyStepOnDrag(true)

    -- Title
    slider.Text:SetFontObject(WildThingsFont13)
    slider.Text:SetText(title)
    slider.Text:SetTextColor(Theme.TEXT.r, Theme.TEXT.g, Theme.TEXT.b, Theme.TEXT.a)
    slider.Text:SetShadowOffset(0, 0)

    -- Low text
    slider.Low:SetFontObject(WildThingsFont13)
    slider.Low:SetText(min)
    slider.Low:SetTextColor(Theme.TEXT_DIM.r, Theme.TEXT_DIM.g, Theme.TEXT_DIM.b, Theme.TEXT_DIM.a)
    slider.Low:SetShadowOffset(0, 0)
    slider.Low:ClearAllPoints()
    slider.Low:SetPoint("TOPLEFT", slider, "BOTTOMLEFT", 2, 0)

    -- High text
    slider.High:SetFontObject(WildThingsFont13)
    slider.High:SetText(max)
    slider.High:SetTextColor(Theme.TEXT_DIM.r, Theme.TEXT_DIM.g, Theme.TEXT_DIM.b, Theme.TEXT_DIM.a)
    slider.High:SetShadowOffset(0, 0)
    slider.High:ClearAllPoints()
    slider.High:SetPoint("TOPRIGHT", slider, "BOTTOMRIGHT", -2, 0)

    -- Background / track: thin white-alpha wash (BORDER_STRONG is the closest token to the
    -- design's "thin white-16% track")
    slider.background = slider:CreateTexture(nil, "BACKGROUND")

    slider.background:SetPoint("TOPLEFT", slider.NineSlice, "TOPLEFT", 0, -3)
    slider.background:SetPoint("BOTTOMRIGHT", slider.NineSlice, "BOTTOMRIGHT", 0, 3)
    slider.background:SetColorTexture(Theme.BORDER_STRONG.r, Theme.BORDER_STRONG.g, Theme.BORDER_STRONG.b, Theme.BORDER_STRONG.a)
    slider.background:SetSnapToPixelGrid(false)
    slider.background:SetTexelSnappingBias(0)

    -- Accent fill: tracks the thumb position from the track's left edge, resized inside the
    -- existing OnValueChanged handler below (alongside slider.Current:SetText) — no second
    -- OnValueChanged script is added.
    slider.fill = slider:CreateTexture(nil, "ARTWORK")
    slider.fill:SetPoint("TOPLEFT", slider.background, "TOPLEFT", 0, 0)
    slider.fill:SetPoint("BOTTOMLEFT", slider.background, "BOTTOMLEFT", 0, 0)
    slider.fill:SetColorTexture(Theme.ACCENT.r, Theme.ACCENT.g, Theme.ACCENT.b, Theme.ACCENT.a)
    slider.fill:SetWidth(0.0001)
    slider.fill:SetSnapToPixelGrid(false)
    slider.fill:SetTexelSnappingBias(0)

    -- Rounded overlay-clip primitive (ROUND-04/05, spike-002): one shared mask clips both
    -- the track and the fill at once. The fill's live right edge (mid-track until 100%)
    -- falls outside the mask's right-corner region and stays a square progress cut, which is
    -- correct (Pattern 3) -- only its left edge follows the track's rounded left end.
    --
    -- CR-02 fix: AddRoundedMask defaults to sizing the mask to `slider` itself, but
    -- slider.background (the actually-masked, visible track) is vertically inset ~3px from
    -- slider's own top/bottom edges (see the anchors above) -- less than the 4px corner-curve
    -- band, so the un-repointed mask's corners wouldn't line up with the track's real
    -- corners. Re-anchor the mask to slider.background's own bounds instead (matching
    -- ProgressBar.lua's equivalent code, where bar.track:SetAllPoints() already matches
    -- bar's bounds exactly, so no such mismatch exists there).
    WTUI:AddRoundedMask(slider, Theme.RADIUS_SM)
    slider.roundedMask:ClearAllPoints()
    slider.roundedMask:SetPoint("TOPLEFT", slider.background, "TOPLEFT")
    slider.roundedMask:SetPoint("BOTTOMRIGHT", slider.background, "BOTTOMRIGHT")
    slider.background:AddMaskTexture(slider.roundedMask)
    slider.fill:AddMaskTexture(slider.roundedMask)

    -- Thumb: recolor the native thumb texture to Theme.WT_WHITE (round corners are not
    -- natively supported and are out of scope, POLISH-01)
    local thumbTexture = slider:GetThumbTexture()
    if thumbTexture then
        thumbTexture:SetVertexColor(Theme.WT_WHITE.r, Theme.WT_WHITE.g, Theme.WT_WHITE.b, Theme.WT_WHITE.a)
    end

    -- Current value box: small bordered box (INPUT_BG background, rounded border,
    -- Mono-Num font), positioned to the right of the track's background. Anchored to
    -- slider.background.RIGHT so it floats with the visible track regardless of how
    -- wide the parent stretches the slider frame itself.
    slider.valueBox = CreateFrame("Frame", nil, slider)
    slider.valueBox:SetSize(36, DEFAULT_HEIGHT)
    slider.valueBox:SetPoint("LEFT", slider.background, "RIGHT", Theme.GAP, 0)

    slider.valueBox.tex = slider.valueBox:CreateTexture(nil, "BACKGROUND")
    slider.valueBox.tex:SetAllPoints()
    slider.valueBox.tex:SetColorTexture(Theme.INPUT_BG.r, Theme.INPUT_BG.g, Theme.INPUT_BG.b, Theme.INPUT_BG.a)
    slider.valueBox.tex:SetSnapToPixelGrid(false)
    slider.valueBox.tex:SetTexelSnappingBias(0)

    -- valueBox rounds independently -- a separate 36x20 frame with its own bounds, distinct
    -- from the track's mask.
    WTUI:AddRoundedMask(slider.valueBox, Theme.RADIUS_SM)
    slider.valueBox.tex:AddMaskTexture(slider.valueBox.roundedMask)

    WTUI:AddRoundedBorder(slider.valueBox, Theme.RADIUS_SM, 1)
    slider.valueBox:SetBorderColor(Theme.BORDER_STRONG.r, Theme.BORDER_STRONG.g, Theme.BORDER_STRONG.b, Theme.BORDER_STRONG.a)

    slider.Current = slider.valueBox:CreateFontString()

    slider.Current:SetFontObject(WildThingsFontNum13)
    slider.Current:SetPoint("CENTER", slider.valueBox, "CENTER", 0, 0)
    slider.Current:SetTextColor(Theme.TEXT.r, Theme.TEXT.g, Theme.TEXT.b, Theme.TEXT.a)
    slider.Current:SetText(string.format("%d", math.floor(max / 2)))

    slider:SetScript(
        "OnValueChanged",
        function(_, value)
            -- Live visual update stays unconditional during drag ticks.
            slider.Current:SetText(string.format("%d", value))

            -- Fill tracks the thumb CENTER when positioned, falls back to linear
            -- pct during construction before the frame is laid out (GetLeft→nil).
            local thumbTex = slider:GetThumbTexture()
            local trackLeft = slider.background:GetLeft()
            if thumbTex and thumbTex:GetLeft() and trackLeft then
                local thumbCx = thumbTex:GetLeft() + thumbTex:GetWidth() / 2
                slider.fill:SetWidth(math.max(0.0001, thumbCx - trackLeft))
            else
                local range = max - min
                local pct = range > 0 and (value - min) / range or 0
                if pct < 0 then pct = 0 elseif pct > 1 then pct = 1 end
                slider.fill:SetWidth(math.max(0.0001, slider.background:GetWidth() * pct))
            end

            -- The caller's set only fires once construction/deferred-sync writes have
            -- finished (initialized) and never while a drag is in progress (dragging is
            -- released via OnMouseUp below, which commits explicitly).
            if initialized and not dragging then
                OnValueChanged(value)
            end
        end
    )

    slider:SetScript("OnMouseDown", function(_, button)
        if button == "LeftButton" then dragging = true end
    end)
    slider:SetScript(
        "OnMouseUp",
        function(_self, button)
            if button == "LeftButton" then
                dragging = false
                OnValueChanged(_self:GetValue())
            end
        end
    )

    -- Guarded programmatic write: brackets the native SetValue with initialized = false so
    -- its synchronous OnValueChanged fire only updates visuals, never the caller's set. Used
    -- for construction-time sync, the deferred first-sync below, and the Task-2 inline
    -- EditBox commit (same closure, called as a plain local).
    local function CommitValue(value)
        initialized = false
        slider:SetValue(value)
        initialized = true
    end

    -- Suppress-guarded external setter (WTUI-04 BuildWidgets' Refresh contract): mirrors
    -- CommitValue so a programmatic re-sync from outside this file never re-fires the
    -- caller's OnValueChanged/set, unlike a raw slider:SetValue(...) call.
    function slider:SetValueSilent(value)
        CommitValue(value)
    end

    slider.NineSlice.TopEdge:Hide()
    slider.NineSlice.BottomEdge:Hide()
    slider.NineSlice.LeftEdge:Hide()
    slider.NineSlice.RightEdge:Hide()

    slider.NineSlice.TopLeftCorner:Hide()
    slider.NineSlice.TopRightCorner:Hide()
    slider.NineSlice.BottomLeftCorner:Hide()
    slider.NineSlice.BottomRightCorner:Hide()

    slider.NineSlice.Center:Hide()

    -- Offsets carry over unchanged: the track's border pulls in 3px vertically so it hugs
    -- the bar rather than the taller hit frame.
    WTUI:AddRoundedBorder(slider, Theme.RADIUS_SM, 1, nil, 0, -3)
    slider:SetBorderColor(Theme.BORDER_STRONG.r, Theme.BORDER_STRONG.g, Theme.BORDER_STRONG.b, Theme.BORDER_STRONG.a)

    if initialValue then
        CommitValue(initialValue)
    else
        CommitValue(math.floor(max / 2))
    end

    -- Deferred re-sync: slider.background's width is derived purely from anchor points
    -- relative to slider.NineSlice (no explicit SetWidth), so GetWidth() on this same-frame,
    -- not-yet-laid-out construction pass can read 0, leaving slider.fill invisible until the
    -- first drag. Mirrors EditBox:Refresh()'s C_Timer.After(0, ...) deferred-sync pattern.
    -- Calls the OnValueChanged script directly (not CommitValue/SetValue) because SetValue
    -- no-ops when the value is unchanged, but the visual recompute still needs to run once
    -- slider.background has a real width; bracketed with initialized = false so this forced
    -- visual-only call never fires the caller's set.
    C_Timer.After(0, function()
        initialized = false
        slider:GetScript("OnValueChanged")(slider, slider:GetValue())
        initialized = true
    end)

    -- Right-click exact-value entry: an inline EditBox overlay reusing the existing EditBox
    -- widget's numeric clamp (SetMinimum/EnsureNumeric) instead of a hand-rolled tonumber/
    -- clamp. Built lazily on first right-click. Enter (via EditBox's own OnEnterPressed ->
    -- ClearFocus) and focus-loss commit through CommitValue (fires the caller's set once);
    -- Escape cancels with no change.
    local inlineEditBox
    local cancelled = false

    local function ShowInlineEditBox()
        slider.Current:Hide()

        if not inlineEditBox then
            inlineEditBox = namespace:CreateEditBox(slider.valueBox, "", function() end)
            inlineEditBox:SetPoint("TOPLEFT", slider.valueBox, "TOPLEFT", 0, 0)
            inlineEditBox:SetPoint("BOTTOMRIGHT", slider.valueBox, "BOTTOMRIGHT", 0, 0)
            inlineEditBox:SetNumeric(true)
            inlineEditBox:SetMinimum(min)

            -- HookScript (not SetScript) preserves EditBox's own EnsureNumeric/min-clamp
            -- OnEditFocusLost behavior; this hook runs after it, so currentValue is already
            -- a number >= min by the time it runs here. Enter routes here too, since the
            -- widget's own OnEnterPressed script calls ClearFocus(), which fires this event.
            inlineEditBox:HookScript(
                "OnEditFocusLost",
                function()
                    inlineEditBox:Hide()
                    slider.Current:Show()

                    if not cancelled then
                        local value = inlineEditBox.currentValue
                        if value > max then value = max end
                        CommitValue(value)
                    end
                    cancelled = false
                end
            )

            inlineEditBox:SetScript(
                "OnEscapePressed",
                function()
                    cancelled = true
                    inlineEditBox:ClearFocus()
                end
            )
        end

        inlineEditBox:SetText(string.format("%d", slider:GetValue()))
        inlineEditBox:Show()
        inlineEditBox:SetFocus()
        inlineEditBox:HighlightText()
    end

    slider.valueBox:EnableMouse(true)
    slider.valueBox:SetScript(
        "OnMouseUp",
        function(_, mouseButton)
            if mouseButton == "LeftButton" then
                ShowInlineEditBox()
            end
        end
    )

    return slider
end
