-- zSettingsFrame/zSF_Init.lua
-- Namespace, public API, and internal shared state for the zSettingsFrame framework.
-- Addon-agnostic: no references to any specific addon symbol.

local MAJOR, MINOR = "zSettingsFrame-1.0", 5
local _lib = LibStub:NewLibrary(MAJOR, MINOR)
if not _lib then return end

-- WoW passes the host addon name as the first vararg to every file it loads.
-- Since this library is embedded inside the host addon, we capture it here once.
local _ZSF_HOST_ADDON = ...

--- Returns the full WoW interface path for a file under this library's Assets folder.
---@param file string  Filename (e.g. "close-button.png")
---@return string
function zSF_AssetPath(file)
    return "Interface\\AddOns\\" .. _ZSF_HOST_ADDON .. "\\Libs\\zSettingsFrame-1.0\\Assets\\" .. file
end

---@class zSF_Adapter
---@field addonName   string                        Name shown in the WoW Settings panel.
---@field addonFolder string|nil                    Folder name of the host addon if it differs from `addonName` (e.g. `"!MyAddon"` vs `"My Addon"`).  Used when querying `C_AddOns.GetAddOnMetadata`.  Defaults to `addonName` when omitted.
---@field schema      table                         Root schema table (array of zSF_SchemaEntry).
---@field get         fun(key: string): any         Read a setting value from the DB.
---@field set         fun(key: string, value: any)  Write a setting value to the DB.
---@field getDefault  fun(key: string): any         Read the default value for a key.
---@field reset       fun(keys: string[])           Reset a list of keys to their defaults.
---@field panelWidth  number|nil                    Panel width in pixels (default: 960).
---@field panelHeight number|nil                    Panel height in pixels (default: 600).
---@field onPanelShow fun()|nil                     Called each time the settings panel opens.
---@field onPanelHide fun()|nil                     Called each time the settings panel closes.
---@field fontStyles  table<string, string>|nil     Optional locale-keyed font map: `{ enUS = "Interface\\...", koKR = "..." }`.  The client locale (`GetLocale()`) is used to pick the path.  Locales not in the table fall back to `STANDARD_TEXT_FONT` (letting WoW render native glyphs).  The built-in default maps `enUS` to the bundled OpenSans-CondBold.ttf; supply your own `enUS` entry to override it.
---@field navAnim         string|nil                    Nav item click animation: "flash" (streak sweep) | "glide" (expand then slide off). Default: "glide".
---@field fadeInAnimation string|nil                    Content panel transition when switching nav items or tabs: "slide" (fade + slide from nav side) | nil/"fade" (alpha fade only). Default: fade.
---@field attachOptions   boolean|nil                   Register the zSF panel directly as the WoW Settings canvas (true / nil) so the panel opens inside the Blizzard settings window.  Pass false to show a stub "Open Options" button instead.  Default: true.
---@field themeColors     { primary: number[]|ColorMixin, secondary: number[]|ColorMixin }|nil  Optional.  Theme accent colors: `primary` and `secondary` are `{ r, g, b }` tables or ColorMixin values (e.g. `RAID_CLASS_COLORS["WARLOCK"]`).  Defaults to `ZSF.CLASS_EVOKER` (teal) / `ZSF.CLASS_PALADIN` (pink) if omitted.
---@field subscribe       fun(callback: fun()): fun()|nil  Optional.  Called when the panel opens.  Receives a callback that should be fired whenever external code changes a setting.  Must return an unsubscribe function.  zSF calls the unsubscribe function when the panel closes.
---@field tabRowMax       number|nil  Max tabs per row in the standalone panel. If a tab list exceeds this, a second row is added. Default: 6.
---@field tabRowMaxBlizz  number|nil  Max tabs per row when hosted inside the Blizzard Settings window. Default: 6.
---@field navWidthBlizzard number|nil  Width in pixels of the nav sidebar when hosted inside the Blizzard Settings window. Defaults to half of the normal sidebar width (about 112px) when omitted.
---@field bgTopLayer      string|nil  Image path for the top background layer, or "off" to hide it entirely. Defaults to the built-in bg-top.png.
---@field bgTopLayerColor number[]|ColorMixin|"off"|nil  RGB table { r, g, b } or ColorMixin to desaturate + tint the top layer, or "off" to show it as-is without desaturation. Defaults to the primary theme colour.
---@field bgBottomLayer      string|nil  Image path for the bottom background layer, or "off" to hide it entirely. Defaults to the built-in bg-bottom.png.
---@field bgBottomLayerColor number[]|ColorMixin|"off"|nil  RGB table { r, g, b } or ColorMixin to desaturate + tint the bottom layer, or "off" to show it as-is without desaturation. Defaults to the secondary theme colour.
---@field showScaleDropdown   boolean|nil  Show a window scale dropdown in the titlebar bottom-right. If enabled, users can scale the panel (75%-150% range) with instant animation. Default: false.
---@field scaleDBKey          string|nil  DB key used to persist the scale setting. Only used if `showScaleDropdown` is true. Library calls `adapter.get(key)` and `adapter.set(key, value)` to manage persistence. Default: "panelScale".
---@field colors             { sidebarBg: number[]|ColorMixin|nil, rowHover: number[]|ColorMixin|nil, border: number[]|ColorMixin|nil, label: number[]|ColorMixin|nil, desc: number[]|ColorMixin|nil, headerText: number[]|ColorMixin|nil, disabled: number[]|ColorMixin|nil, disabledAlpha: number|nil, tabActive: number[]|ColorMixin|nil, separator: number[]|ColorMixin|nil, rowStripeOdd: number[]|ColorMixin|nil, rowStripeEven: number[]|ColorMixin|nil, colDivider: number[]|ColorMixin|nil, toggleIndicatorColor: number[]|ColorMixin|nil }|nil  Optional per-panel chrome color overrides applied at build time.  Each key maps to the corresponding `ZSF.COLOR_*` / `ZSF.DISABLED_ALPHA` value in the ZSF namespace table.  Color values are `{ r, g, b, a }` arrays or ColorMixin objects (e.g. `RAID_CLASS_COLORS["WARRIOR"]`); `disabledAlpha` is a plain number.  `toggleIndicatorColor` tints the knob of every toggle pill when ON.  Omitted keys fall back to the namespace default.
---@field cpuProfile         boolean|nil  Show this addon's CPU share (% of app time, 1-second rolling average) in the status bar.  Polled every second while the panel is open.  Only active when `C_AddOnProfiler` is enabled.
---@field memoryProfile      boolean|nil  Show this addon's memory usage in the status bar.  Polled at most once per 15 seconds while the panel is open; shared across all open zSF panels to avoid redundant calls.

---@class zSF_NavInfo
---@field name               string
---@field description        string|nil   Short blurb shown in the content header.
---@field showWhen           fun(): boolean|nil  Hides this nav item when it returns false.
---@field disableWhen        fun(): boolean|nil  Greys out and blocks interaction when it returns true.
---@field children           string[]|nil navChild sidebar labels.
---@field hasOwnContent      boolean
---@field childSchemas       table[]|nil  Per-child widget schema arrays.
---@field ownSchema          table[]|nil  Widgets belonging to the nav parent.
---@field tabNames           string[]|nil Tab strip labels for nav-level tabs.
---@field tabSchemas         table[]|nil  Per-tab widget schema arrays.
---@field tabShowWhen        table|nil    [ti] = fun(): boolean|nil per nav-level-tab visibility.
---@field tabDisableWhen     table|nil    [ti] = fun(): boolean|nil per nav-level-tab disable.
---@field trackerIDs         table|nil    Per-child trackerID, index-matched to children.
---@field childShowWhen      table|nil    [ci] = fun(): boolean|nil per-navChild visibility.
---@field childDisableWhen   table|nil    [ci] = fun(): boolean|nil per-navChild disable.
---@field childTabNames      table|nil    childTabNames[ci] = string[] of tab labels for navChild ci.
---@field childTabSchemas    table|nil    childTabSchemas[ci] = table[] of per-tab widget schemas for navChild ci.
---@field childTabShowWhen   table|nil    childTabShowWhen[ci][ti] = fun(): boolean|nil per navChild per-tab visibility.
---@field childTabDisableWhen table|nil   childTabDisableWhen[ci][ti] = fun(): boolean|nil per navChild per-tab disable.

---@class zSF_AddonContext
---@field _adapter       zSF_Adapter
---@field _widgets       table[]
---@field _categoryID    any
---@field _panel         Frame|nil
---@field _built         boolean
---@field _navHierarchy  table[]|nil
---@field _themeColors   table|nil
---@field _selfWriting   boolean
---@field FONTOBJ_LARGE    Font
---@field FONTOBJ_TITLEBAR Font  -- titlebar nav name text
---@field FONTOBJ_HEADING  Font  -- standard section headers
---@field FONTOBJ_NORMAL   Font
---@field FONTOBJ_SMALL    Font
---@field ShowConfirm    fun(self:zSF_AddonContext, text:string, onAccept:fun(), onRevert:fun()|nil)
---@field ShowInput      fun(self:zSF_AddonContext, config:table)
---@field ShowOutput     fun(self:zSF_AddonContext, config:table)
---@field ShowColorPicker fun(self:zSF_AddonContext, config:table)

---@class zSettingsFrameNS
---@field _contexts  table<string, zSF_AddonContext>  Per-addon context objects keyed by addonName.
---@field _buildCtx  zSF_AddonContext|nil             Active context during a build pass; nil outside builds.
---@field _dialogs   table                            Dialog factory functions, keyed by type. Set by Widgets.lua.
zSettingsFrame = _lib

--- Single namespace table that holds all internal style constants, easing functions,
--- and optional consumer-set font objects. Bridge files should assign into this table
--- rather than writing to bare `ZSF_*` globals.
---@type table
ZSF = ZSF or {}

-- Shared dispatch tables; all per-addon state lives in the context objects below.
zSettingsFrame._contexts = zSettingsFrame._contexts or {}  ---@type table<string, zSF_AddonContext>
zSettingsFrame._buildCtx = nil  ---@type zSF_AddonContext|nil  Set during build; read by Primitives, Widgets, Constructor.

-- ── Nav hierarchy ─────────────────────────────────────────────────────────────
--- Walk the top-level schema and build the nav hierarchy consumed by the sidebar.
---@param schema table[]
---@return zSF_NavInfo[]
local function ResolveNavHierarchy(schema)
    local navs = {}
    for _, entry in ipairs(schema) do
        if entry.widgetType == "nav" then
            local info = {
                name          = entry.name,
                description   = entry.desc,
                showWhen      = entry.showWhen,
                disableWhen   = entry.disableWhen,
                children      = nil,
                hasOwnContent = false,
                childSchemas  = nil,
                ownSchema     = nil,
                tabNames      = nil,
                tabSchemas    = nil,
                trackerIDs    = nil,
            }

            local navChildNames, navChildSchemas, navChildTrackerIDs = {}, {}, {}
            local navChildShowWhen, navChildDisableWhen              = {}, {}
            local childTabNames, childTabSchemas                     = {}, {}
            local childTabShowWhen, childTabDisableWhen              = {}, {}
            local hasChildTabs = false
            local tabNames, tabSchemas                               = {}, {}
            local tabShowWhen, tabDisableWhen                        = {}, {}
            local ownWidgets = {}

            if entry.children then
                for _, child in ipairs(entry.children) do
                    if child.widgetType == "navChild" then
                        local ci = #navChildNames + 1
                        navChildNames[ci]       = child.name
                        navChildTrackerIDs[ci]  = child.trackerID
                        navChildShowWhen[ci]    = child.showWhen
                        navChildDisableWhen[ci] = child.disableWhen
                        -- Separate tab entries from plain widgets inside this navChild.
                        local rawSchema = child.children or {}
                        local childOwn, cTabNames, cTabSchemas = {}, {}, {}
                        local cTabShowWhen, cTabDisableWhen    = {}, {}
                        for _, w in ipairs(rawSchema) do
                            if w.widgetType == "tab" then
                                local cti          = #cTabNames + 1
                                cTabNames[cti]     = w.name
                                cTabSchemas[cti]   = w.children or {}
                                cTabShowWhen[cti]  = w.showWhen
                                cTabDisableWhen[cti] = w.disableWhen
                            else
                                childOwn[#childOwn + 1] = w
                            end
                        end
                        if #cTabNames > 0 then
                            hasChildTabs              = true
                            childTabNames[ci]         = cTabNames
                            childTabSchemas[ci]       = cTabSchemas
                            childTabShowWhen[ci]      = cTabShowWhen
                            childTabDisableWhen[ci]   = cTabDisableWhen
                            navChildSchemas[ci]       = childOwn
                        else
                            navChildSchemas[ci] = rawSchema
                        end
                    elseif child.widgetType == "tab" then
                        local ti           = #tabNames + 1
                        tabNames[ti]       = child.name
                        tabSchemas[ti]     = child.children or {}
                        tabShowWhen[ti]    = child.showWhen
                        tabDisableWhen[ti] = child.disableWhen
                    else
                        ownWidgets[#ownWidgets + 1] = child
                    end
                end
            end

            if #navChildNames > 0 then
                info.children         = navChildNames
                info.childSchemas     = navChildSchemas
                info.trackerIDs       = navChildTrackerIDs
                info.childShowWhen    = navChildShowWhen
                info.childDisableWhen = navChildDisableWhen
            end
            if #ownWidgets > 0 then
                info.hasOwnContent = true
                info.ownSchema     = ownWidgets
            end
            if #tabNames > 0 then
                info.tabNames      = tabNames
                info.tabSchemas    = tabSchemas
                info.tabShowWhen   = tabShowWhen
                info.tabDisableWhen = tabDisableWhen
            end
            if hasChildTabs then
                info.childTabNames        = childTabNames
                info.childTabSchemas      = childTabSchemas
                info.childTabShowWhen     = childTabShowWhen
                info.childTabDisableWhen  = childTabDisableWhen
            end

            navs[#navs + 1] = info
        end
    end
    return navs
end

--- Normalize any incoming color to a plain numeric array { r, g, b [, a] } so that
--- all internal code can safely index [1]/[2]/[3] or call unpack() on it.
--- Accepts: array { r, g, b [, a] }, standalone ColorMixin/table { r=, g=, b= [, a=] },
--- or one-element wrapper like { RAID_CLASS_COLORS["PRIEST"] }.
--- Non-table values (nil, "off") are returned unchanged so optional fields stay optional.
---@param c table|string|nil
---@return table|string|nil
function ZSF.NormalizeColor(c)
    if type(c) ~= "table" then
        return c
    end

    -- Already a numeric array: keep as-is.
    if type(c[1]) == "number" then
        return c
    end

    -- One-level wrapper: { ColorMixin } or { {r,g,b} }.
    if #c == 1 and type(c[1]) == "table" then
        local unwrapped = ZSF.NormalizeColor(c[1])
        if type(unwrapped) == "table" and type(unwrapped[1]) == "number" then
            return unwrapped
        end
        -- Fall through to next normalization; allow booleans/nils to be handled.
        c = c[1]
    end

    -- Field-keyed table or ColorMixin.
    if c.r or c.g or c.b or c.a then
        return { c.r or 0, c.g or 0, c.b or 0, c.a }
    end

    -- Fallback: return as-is; caller may have a default fallback.
    return c
end

local NormalizeRGB = ZSF.NormalizeColor

--- Content panels are deferred to the first OnShow via RebuildContent so no
--- widget frames are allocated until the user actually opens settings.
--- Safe to call multiple times; no-ops after the first successful build.
---@param ctx zSF_AddonContext
local function ensureBuilt(ctx)
    if ctx._built then return end
    local adapter = ctx._adapter
    if not adapter then return end
    local colors = ctx._themeColors or {}
    local pri    = NormalizeRGB(colors.primary   or ZSF.DEFAULT_PRIMARY)
    local sec    = NormalizeRGB(colors.secondary or ZSF.DEFAULT_SECONDARY)
    local prevCtx = zSettingsFrame._buildCtx
    zSettingsFrame._buildCtx = ctx
    local p = ctx._buildPanel(adapter, pri, sec)
    ctx._panel = p
    ctx._buildSidebarNav(p.navContent or p.sidebar, ctx._navHierarchy or {})
    ctx._built = true
    zSettingsFrame._buildCtx = prevCtx
end

--- Register a consuming addon with the framework.
--- Each addon gets an isolated context object (panel, widgets, fonts, etc.).
--- Returns the context; callers should capture it for ctx:Open() / ctx:RefreshAll().
---@param adapter zSF_Adapter
---@return zSF_AddonContext
function zSettingsFrame.Register(adapter)
    assert(type(adapter.addonName)  == "string",     "zSettingsFrame.Register: addonName must be a string")
    assert(not zSettingsFrame._contexts[adapter.addonName],
        "zSettingsFrame.Register: already registered: " .. adapter.addonName)
    assert(type(adapter.get)        == "function",   "zSettingsFrame.Register: get must be a function")
    assert(type(adapter.set)        == "function",   "zSettingsFrame.Register: set must be a function")
    assert(type(adapter.getDefault) == "function",   "zSettingsFrame.Register: getDefault must be a function")
    assert(type(adapter.reset)      == "function",   "zSettingsFrame.Register: reset must be a function")

    ---@type zSF_AddonContext
    local ctx = {
        _adapter       = adapter,
        _widgets       = {},
        _categoryID    = nil,
        _panel         = nil,
        _built         = false,
        _navHierarchy  = nil,
        _themeColors   = nil,
        _selfWriting   = false,
    }

    -- Resolve nav hierarchy from the adapter schema.
    if type(adapter.schema) == "table" then
        ctx._navHierarchy = ResolveNavHierarchy(adapter.schema)
    end

    -- Theme colors from the adapter — normalize to plain arrays on the way in so
    -- all downstream code (dialogs, panel build) can safely array-index or unpack().
    if adapter.themeColors then
        ctx._themeColors = {
            primary   = ZSF.NormalizeColor(adapter.themeColors.primary),
            secondary = ZSF.NormalizeColor(adapter.themeColors.secondary),
        }
    end

    -- Resolve the font path for the current client locale.
    -- fontStyles is a locale-keyed table; enUS falls back to the bundled
    -- OpenSans-CondBold when not explicitly mapped.  Any locale absent from
    -- the table gets STANDARD_TEXT_FONT so native glyphs render correctly.
    -- Unique font-object names prevent conflicts across addons and reloads.
    local locale    = GetLocale()
    local userFonts = type(adapter.fontStyles) == "table" and adapter.fontStyles or {}
    local fontPath
    if userFonts[locale] then
        fontPath = userFonts[locale]
    elseif locale == "enUS" then
        fontPath = zSF_AssetPath("Fonts\\OpenSans-CondBold.ttf")
    else
        fontPath = STANDARD_TEXT_FONT
    end
    ctx.FONTOBJ_LARGE    = CreateFont("zSF_FontLarge_"   .. adapter.addonName)
    ctx.FONTOBJ_LARGE:SetFont(fontPath, 16, "")
    ctx.FONTOBJ_TITLEBAR = CreateFont("zSF_FontTitleBar_" .. adapter.addonName)
    ctx.FONTOBJ_TITLEBAR:SetFont(fontPath, 32, "")  -- big titlebar nav label
    ctx.FONTOBJ_HEADING  = CreateFont("zSF_FontHeading_" .. adapter.addonName)
    ctx.FONTOBJ_HEADING:SetFont(fontPath, 18, "")  -- standard section headers
    ctx.FONTOBJ_NORMAL   = CreateFont("zSF_FontNormal_"  .. adapter.addonName)
    ctx.FONTOBJ_NORMAL:SetFont(fontPath, 12, "")
    ctx.FONTOBJ_SMALL    = CreateFont("zSF_FontSmall_"   .. adapter.addonName)
    ctx.FONTOBJ_SMALL:SetFont(fontPath, 10, "")

    -- Attach panel-building methods (defined in zSF_Panel.lua) to the context.
    zSF_InitPanelInstance(ctx)

    -- Public context methods.
    function ctx:Open()
        local ad = self._adapter
        if ad.attachOptions ~= false
        and SettingsPanel and SettingsPanel:IsShown() then
            if self._categoryID then
                Settings.OpenToCategory(self._categoryID)
            end
            return
        end
        ensureBuilt(self)
        local p = self._panel
        if not p then return end
        p:SetParent(UIParent)
        p:SetFrameStrata("FULLSCREEN_DIALOG")
        -- Raise above any other zSF panels that are already visible so that
        -- widgets from different addons never bleed through each other.
        local topLevel = 0
        for _, other in pairs(zSettingsFrame._contexts) do
            if other._panel and other._panel ~= p and other._panel:IsShown() then
                -- Use the border's level if available — it's always the highest
                -- layer on a panel, so the incoming panel must start above it.
                local lvl = (other._panel._border and other._panel._border:GetFrameLevel())
                         or other._panel:GetFrameLevel()
                if lvl > topLevel then topLevel = lvl end
            end
        end
        local baseLevel = math.max(100, topLevel + 100)
        p:SetFrameLevel(baseLevel)
        if p._border then p._border:SetFrameLevel(baseLevel + 200) end
        p:SetSize(ad.panelWidth or 1080, ad.panelHeight or 800)
        p:ClearAllPoints()
        p:SetPoint("CENTER")
        if p:IsShown() then p:Hide() end
        p:Show()
    end

    function ctx:RefreshAll(force)
        for _, widget in ipairs(self._widgets) do
            widget:Refresh(force)
        end
        -- Re-evaluate nav/child/tab showWhen and disableWhen conditions.
        if self._refreshNav  then self._refreshNav()  end
        if self._refreshTabs then self._refreshTabs() end
    end

    function ctx:ShowConfirm(text, onAccept, onRevert)
        zSettingsFrame._dialogs.confirm(self._adapter.addonName)
            .show(text, onAccept, onRevert, self._themeColors)
    end

    function ctx:ShowInput(config)
        zSettingsFrame._dialogs.input(self._adapter.addonName)
            .show(config, self._themeColors)
    end

    function ctx:ShowOutput(config)
        zSettingsFrame._dialogs.output(self._adapter.addonName)
            .show(config, self._themeColors)
    end

    function ctx:ShowColorPicker(config)
        zSettingsFrame._dialogs.colorpick(self._adapter.addonName)
            .show(config, self._themeColors)
    end

    zSettingsFrame._contexts[adapter.addonName] = ctx

    -- Wire into WoW Settings on PLAYER_LOGIN when the Settings API is ready.
    local f = CreateFrame("Frame")
    f:RegisterEvent("PLAYER_LOGIN")
    f:SetScript("OnEvent", function(self)
        self:UnregisterEvent("PLAYER_LOGIN")
        self:SetScript("OnEvent", nil)
        if adapter.attachOptions ~= false then
            ensureBuilt(ctx)
            local p = ctx._panel
            if p then
                local category = Settings.RegisterCanvasLayoutCategory(p, adapter.addonName)
                Settings.RegisterAddOnCategory(category)
                ctx._categoryID = category:GetID()
            end
        else
            ctx._registerStubFrame(adapter.addonName)
        end
    end)

    return ctx
end

--- Open a registered addon's settings panel.
--- Pass the addonName to open a specific addon, or omit when only one is registered.
---@param addonName string|nil
function zSettingsFrame.Open(addonName)
    local ctx
    if addonName then
        ctx = zSettingsFrame._contexts[addonName]
    else
        -- Convenience: if exactly one addon is registered, open it.
        local count = 0
        for _, c in pairs(zSettingsFrame._contexts) do
            count = count + 1
            ctx = c
        end
        if count ~= 1 then ctx = nil end
    end
    if ctx then ctx:Open() end
end

--- Refresh all widgets for a specific addon, or all registered addons.
--- Pass addonName to target one addon, omit to refresh every registered addon.
--- Pass force=true to skip change-detection on profile switches.
---@param force     boolean|nil
---@param addonName string|nil
function zSettingsFrame.RefreshAll(force, addonName)
    if addonName then
        local ctx = zSettingsFrame._contexts[addonName]
        if ctx then ctx:RefreshAll(force) end
    else
        for _, ctx in pairs(zSettingsFrame._contexts) do
            ctx:RefreshAll(force)
        end
    end
end
