-- Forge_APIBrowser: browse WoW's Lua API surface as an expandable tree.
--
-- v3 scope (this file):
--   * Cairn-Gui TreeView widget renders a hierarchical view. C_* namespaces
--     are collapsible branches; their member functions are leaves. Top-level
--     globals sit under a synthetic "Top-level globals" branch.
--   * Filter chips (All / C_* / Top-level), live substring search.
--   * Click a leaf to load a call skeleton + the wiki-derived signature
--     into two inline EditBoxes.
--   * Signatures.lua (generated from warcraft.wiki.gg via
--     Wikis/WarcraftWikiCrawler/gen_signatures.py) bundles ~5400 entries.
--     Lookup is a single ns.Signatures[apiPath] hash hit.
--
-- Out of scope for v3 (queued for v4):
--   * Parameter hints / type info (would need parsing the wiki "Arguments"
--     section into structured data, not just the signature line).
--   * Live "Run this" / "Insert into chat" buttons.
--   * Persistent expansion state across sessions.
--   * Auto-update Signatures.lua from the wiki crawler on a schedule.

local ADDON, ns = ...
_G.Forge_APIBrowser = ns


Cairn.Register("CTS_Forge_APIBrowser", ns, {
    dbName = "Forge_APIBrowserDB",
    Log = true, Events = true, Timer = true,
    dbDefaults = { profile = {}, global = {} },
})

local _registry = Cairn.GetRegistry()
local _entry    = _registry["CTS_Forge_APIBrowser"]
local db        = _entry and _entry.db
local addon     = _entry and _entry.cairnAddon
ns.db, ns.addon = db, addon


-- ---------------------------------------------------------------------------
-- Tunables
-- ---------------------------------------------------------------------------

local TREE_ROW_HEIGHT     = 18
local TREE_INDENT         = 16

-- Layout offsets used by relayout(). Top section: heading + hint + toolbar
-- + skeleton row + signature row + gaps.
local TOP_RESERVED        = 176  -- v3: +34 for the signature display row
local BOTTOM_PAD          = 10
local SIDE_PAD            = 10


-- ---------------------------------------------------------------------------
-- Module-scope state
-- ---------------------------------------------------------------------------

local _allEntries     = nil
local _searchFilter   = ""
local _filterMode     = "all"  -- "all" | "cstar" | "toplevel"

local _pane
local _searchBox
local _statusLabel
local _filterChips    = {}
local _skeletonBox
local _signatureBox      -- v3: read-only display of the wiki signature
local _treeScroll
local _treeView


-- Forward declarations.
local refreshTree
local refreshFilterChips


-- ---------------------------------------------------------------------------
-- Show the call skeleton inline
-- ---------------------------------------------------------------------------
-- The previous v1 attempt used StaticPopupDialogs. In-game test showed
-- the popup never visibly opened, likely because Forge windows render
-- at HIGH strata (Cairn-Gui Window MINOR 3) above DIALOG-strata popups.
-- The inline EditBox dodges the issue and is a better UX -- skeleton
-- stays visible while the user keeps browsing.

local function showCallSkeleton(text)
    if _skeletonBox and _skeletonBox.Cairn then
        _skeletonBox.Cairn:SetText(tostring(text or ""))
        if _skeletonBox.Cairn.HighlightText then
            _skeletonBox.Cairn:HighlightText()
        end
        if _skeletonBox.Cairn.SetFocus then
            _skeletonBox.Cairn:SetFocus()
        end
    end
end


-- v3: signature lookup. ns.Signatures is the bundled wiki-derived table
-- (Signatures.lua, generated by Wikis/WarcraftWikiCrawler/gen_signatures.py).
-- Returns nil when the function isn't in the index, which happens for
-- LoD addon globals and any API too new for the last wiki crawl.
local function getSignature(apiPath)
    if not (ns.Signatures and apiPath) then return nil end
    return ns.Signatures[apiPath]
end


local function showSignature(text)
    if not (_signatureBox and _signatureBox.Cairn) then return end
    if text and text ~= "" then
        _signatureBox.Cairn:SetText(text)
    else
        _signatureBox.Cairn:SetText("")
    end
end


-- ---------------------------------------------------------------------------
-- Discovery
-- ---------------------------------------------------------------------------
-- Walks _G once. Collects: top-level functions + everything inside any
-- table whose name starts with "C_". On 120005 every C_* namespace is
-- flat (members are functions, not nested tables of functions), so we
-- only recurse one level.

local function discoverAPI()
    local entries = {}

    for k, v in pairs(_G) do
        if type(k) == "string" then
            local tv = type(v)
            if tv == "function" then
                entries[#entries + 1] = {
                    path  = k,
                    kind  = "function",
                    scope = "toplevel",
                }
            elseif tv == "table" and k:match("^C_") then
                entries[#entries + 1] = {
                    path  = k,
                    kind  = "namespace",
                    scope = "cstar",
                }
                for k2, v2 in pairs(v) do
                    if type(k2) == "string" and type(v2) == "function" then
                        entries[#entries + 1] = {
                            path  = k .. "." .. k2,
                            kind  = "function",
                            scope = "cstar",
                            parent = k,
                        }
                    end
                end
            end
        end
    end

    table.sort(entries, function(a, b)
        return a.path:lower() < b.path:lower()
    end)
    return entries
end


-- ---------------------------------------------------------------------------
-- Tree construction
-- ---------------------------------------------------------------------------
-- Build the TreeView's node array from _allEntries + current filters.
--
-- Two top-level branches:
--   __toplevel : synthetic, contains every top-level function leaf
--   C_*        : one branch per C_* namespace; children are its functions
--
-- Filter mode hides whole branch groups. Search filter narrows children
-- by path substring; a C_* branch is also included when its own name
-- matches the search (so typing "C_Map" still surfaces it even when no
-- children match).

local function searchMatches(text)
    if _searchFilter == "" then return true end
    return text:lower():find(_searchFilter:lower(), 1, true) ~= nil
end


local function buildTree()
    if not _allEntries then return {} end
    local cstarBranches  = {}      -- name -> { id, label, aux, children, _kind }
    local cstarOrder     = {}      -- ordered namespace names for stable sort
    local topLevelLeaves = {}

    for _, entry in ipairs(_allEntries) do
        if entry.scope == "toplevel" then
            if searchMatches(entry.path) then
                topLevelLeaves[#topLevelLeaves + 1] = {
                    id    = entry.path,
                    label = entry.path,
                    aux   = "function",
                    _kind = "function",
                }
            end
        elseif entry.scope == "cstar" then
            if entry.kind == "namespace" then
                if not cstarBranches[entry.path] then
                    cstarBranches[entry.path] = {
                        id       = entry.path,
                        label    = entry.path,
                        aux      = "namespace",
                        children = {},
                        _kind    = "namespace",
                    }
                    cstarOrder[#cstarOrder + 1] = entry.path
                end
            elseif entry.kind == "function" and entry.parent then
                local b = cstarBranches[entry.parent]
                if not b then
                    -- Defensive: namespace entry should precede children
                    -- because they sort alphabetically together, but if
                    -- discovery ever changes we don't want to drop functions.
                    b = {
                        id       = entry.parent,
                        label    = entry.parent,
                        aux      = "namespace",
                        children = {},
                        _kind    = "namespace",
                    }
                    cstarBranches[entry.parent] = b
                    cstarOrder[#cstarOrder + 1] = entry.parent
                end
                if searchMatches(entry.path) then
                    local short = entry.path:match("^[^.]+%.(.+)$") or entry.path
                    b.children[#b.children + 1] = {
                        id    = entry.path,
                        label = short,
                        aux   = "function",
                        _kind = "function",
                    }
                end
            end
        end
    end

    local root = {}

    -- Top-level branch.
    if (_filterMode == "all" or _filterMode == "toplevel")
       and #topLevelLeaves > 0 then
        root[#root + 1] = {
            id       = "__toplevel",
            label    = "Top-level globals",
            aux      = ("(%d)"):format(#topLevelLeaves),
            children = topLevelLeaves,
            _kind    = "pseudo",
        }
    end

    -- C_* branches in alphabetical namespace order.
    if _filterMode == "all" or _filterMode == "cstar" then
        for _, name in ipairs(cstarOrder) do
            local b = cstarBranches[name]
            local nameMatches = searchMatches(name)
            -- Show the branch when its own name matches the search,
            -- OR when it has any matching function children.
            if nameMatches or #b.children > 0 then
                b.aux = ("namespace (%d)"):format(#b.children)
                root[#root + 1] = b
            end
        end
    end

    return root
end


-- ---------------------------------------------------------------------------
-- Refresh
-- ---------------------------------------------------------------------------

refreshTree = function()
    if not _treeView then return end
    _treeView.Cairn:SetNodes(buildTree())

    if _statusLabel and _statusLabel.Cairn then
        local total = (_allEntries and #_allEntries) or 0
        local visible = _treeView.Cairn:GetVisibleCount() or 0
        _statusLabel.Cairn:SetText(
            ("|cff888888%d visible / %d total|r"):format(visible, total))
    end
end


refreshFilterChips = function()
    if not _filterChips then return end
    for mode, btn in pairs(_filterChips) do
        if btn.Cairn and btn.Cairn.SetVariant then
            btn.Cairn:SetVariant(mode == _filterMode and "primary" or "ghost")
        end
    end
end


local function setFilterMode(mode)
    _filterMode = mode
    refreshFilterChips()
    refreshTree()
end


-- ---------------------------------------------------------------------------
-- Skeleton text for the copy box
-- ---------------------------------------------------------------------------

local function callSkeleton(node)
    if node._kind == "function" then
        return node.id .. "()"
    end
    if node._kind == "namespace" then
        return node.id
    end
    -- Pseudo branches ("__toplevel"): no useful skeleton.
    return ""
end


-- ---------------------------------------------------------------------------
-- Relayout
-- ---------------------------------------------------------------------------

local function relayout()
    if not (_pane and _treeScroll) then return end
    local paneH = _pane:GetHeight() or 0
    if paneH < 200 then return end
    _treeScroll:ClearAllPoints()
    _treeScroll:SetPoint("TOPLEFT",     _pane, "TOPLEFT",      SIDE_PAD, -TOP_RESERVED)
    _treeScroll:SetPoint("BOTTOMRIGHT", _pane, "BOTTOMRIGHT", -SIDE_PAD,  BOTTOM_PAD)
end


-- ---------------------------------------------------------------------------
-- Build
-- ---------------------------------------------------------------------------

local function build(pane)
    local Gui = LibStub("Cairn-Gui-2.0", true)
    if not Gui then return end
    _pane = pane

    pane.Cairn:SetLayout("Stack",
        { direction = "vertical", gap = 6, padding = 10 })

    Gui:Acquire("Label", pane, { text = "API Browser", variant = "heading" })

    Gui:Acquire("Label", pane, {
        text    = "|cff888888Click a branch to expand. Click a leaf to load a call skeleton below. C_* = modern Blizzard namespaces; top-level = legacy globals.|r",
        variant = "muted",
    })

    -- Toolbar: search + filter chips + status.
    local toolbar = Gui:Acquire("Container", pane, { height = 28 })
    toolbar.Cairn:SetLayout("Stack",
        { direction = "horizontal", gap = 6, padding = 0 })

    _searchBox = Gui:Acquire("EditBox", toolbar, {
        width       = 260,
        height      = 22,
        text        = "",
        placeholder = "Filter by name...",
    })
    _searchBox.Cairn:On("TextChanged", function(_, text)
        _searchFilter = text or ""
        refreshTree()
    end)

    local filterDefs = {
        { mode = "all",      label = "All"       },
        { mode = "cstar",    label = "C_*"       },
        { mode = "toplevel", label = "Top-level" },
    }
    for _, def in ipairs(filterDefs) do
        local chip = Gui:Acquire("Button", toolbar, {
            text = def.label, variant = "ghost", width = 80, height = 22,
        })
        chip.Cairn:On("Click", function() setFilterMode(def.mode) end)
        _filterChips[def.mode] = chip
    end

    -- Clear button: reset search, skeleton, signature, and filter mode
    -- back to defaults so the user can start fresh without typing.
    local clearBtn = Gui:Acquire("Button", toolbar, {
        text = "Clear", variant = "ghost", width = 70, height = 22,
    })
    clearBtn.Cairn:On("Click", function()
        if _searchBox and _searchBox.Cairn then
            _searchBox.Cairn:SetText("")
        end
        _searchFilter = ""
        if _skeletonBox and _skeletonBox.Cairn then
            _skeletonBox.Cairn:SetText("")
        end
        if _signatureBox and _signatureBox.Cairn then
            _signatureBox.Cairn:SetText("")
        end
        setFilterMode("all")
    end)

    _statusLabel = Gui:Acquire("Label", toolbar, {
        text = "|cff888888discovering API...|r", variant = "muted",
    })

    -- Skeleton row: clicking a tree row fills this EditBox.
    local skeletonRow = Gui:Acquire("Container", pane, { height = 26 })
    skeletonRow.Cairn:SetLayout("Stack",
        { direction = "horizontal", gap = 6, padding = 0 })

    Gui:Acquire("Label", skeletonRow, {
        text = "|cff888888Selected:|r", variant = "muted",
    })

    _skeletonBox = Gui:Acquire("EditBox", skeletonRow, {
        width       = 480,
        height      = 22,
        text        = "",
        placeholder = "click a row to load a call skeleton here",
    })

    -- v3: signature display row. Shows the wiki-derived call signature
    -- for the selected function. Read-only-ish: the EditBox is still
    -- editable (so the user can Ctrl-A / Ctrl-C copy), but every leaf
    -- click overwrites it. Reads from ns.Signatures (Signatures.lua).
    local sigRow = Gui:Acquire("Container", pane, { height = 28 })
    sigRow.Cairn:SetLayout("Stack",
        { direction = "horizontal", gap = 6, padding = 0 })

    Gui:Acquire("Label", sigRow, {
        text = "|cff888888Signature:|r", variant = "muted",
    })

    _signatureBox = Gui:Acquire("EditBox", sigRow, {
        width       = 480,
        height      = 22,
        text        = "",
        placeholder = "wiki-derived signature appears here for known functions",
    })

    -- TreeView wrapped in a ScrollFrame. The TreeView auto-grows its
    -- own height to fit visible rows; the ScrollFrame around it gives
    -- vertical scroll when the tree exceeds the visible area.
    _treeScroll = Gui:Acquire("ScrollFrame", pane, {
        bg            = "color.bg.surface",
        border        = "color.border.default",
        borderWidth   = 1,
        showScrollbar = true,
    })
    _treeScroll.Cairn:SetLayoutManual(true)

    local treeContent = _treeScroll.Cairn:GetContent()
    -- The TreeView is the only child of the scroll content. Anchor it
    -- TOPLEFT + TOPRIGHT so its width tracks the scroll viewport. Height
    -- is managed by the TreeView itself.
    _treeView = Gui:Acquire("TreeView", treeContent, {
        nodes     = {},
        rowHeight = TREE_ROW_HEIGHT,
        indent    = TREE_INDENT,
    })
    _treeView.Cairn:SetLayoutManual(true)
    _treeView:ClearAllPoints()
    _treeView:SetPoint("TOPLEFT",  treeContent, "TOPLEFT",  0, 0)
    _treeView:SetPoint("TOPRIGHT", treeContent, "TOPRIGHT", 0, 0)

    _treeView.Cairn:On("Click", function(_, nodeId, node)
        local skeleton = callSkeleton(node)
        if skeleton ~= "" then showCallSkeleton(skeleton) end
        -- v3: also load signature into the sig display. Functions get a
        -- lookup result or "(no signature)"; namespaces clear the field
        -- since they don't have a single call signature.
        if node._kind == "function" then
            local sig = getSignature(node.id)
            showSignature(sig or "|cff888888(no signature)|r")
        else
            showSignature("")
        end
    end)

    -- Track TreeView height into ScrollFrame content height so the
    -- scrollbar reflects the true content size as branches expand /
    -- collapse.
    _treeView:HookScript("OnSizeChanged", function()
        if _treeScroll.Cairn and _treeScroll.Cairn.SetContentHeight then
            _treeScroll.Cairn:SetContentHeight(
                math.max(40, _treeView:GetHeight() or 40))
        end
    end)

    pane:HookScript("OnSizeChanged", relayout)
    relayout()

    _allEntries = discoverAPI()
    setFilterMode(_filterMode)
end


-- ---------------------------------------------------------------------------
-- Tab descriptor
-- ---------------------------------------------------------------------------

ns.descriptor = {
    name        = "API Browser",
    title       = "API Browser",
    order       = 45,
    description = "Browse the WoW Lua API as an expandable tree.",

    OnTabShow = function(pane, mod)
        if not pane.Cairn._builtOnce then
            pane.Cairn._builtOnce = true
            build(pane)
        end
        relayout()
        -- Re-discover so newly-loaded LoD addons get picked up.
        _allEntries = discoverAPI()
        refreshTree()
    end,

    OnTabHide = function(pane, mod)
        -- Nothing to tear down.
    end,
}


-- ---------------------------------------------------------------------------
-- Lifecycle
-- ---------------------------------------------------------------------------

function addon:OnInit()
    if Forge and Forge.Registry and Forge.Registry.Register then
        Forge.Registry.Register(ns.descriptor)
    end
end
