-------------------------------------------------------------------------------
-- SpellSearchBox.lua
-- Spell search predictor for AceGUI-3.0-Search-EditBox, registered as
-- widget type "EditBoxHET_SpellSearch" (the lib prepends "EditBox") —
-- used via dialogControl on the custom-tracker spell inputs.
--
-- Suggestions come ONLY from curated per-class tables — there is no filtering
-- API to classify arbitrary spell IDs. Those tables live in the SpellData_*
-- files, one "kind" each: buffs (SpellData_Buffs.lua, shared with the
-- defensive tracker groups), debuffs (SpellData_Debuffs.lua), abilities and
-- items (SpellData_AbilitiesItems.lua). Anything unlisted is still usable by
-- typing its exact spell/item ID (any valid ID is accepted).
--
-- Four predictors, one per kind, each scoped to its own kind's list:
--   EditBoxHET_SpellSearch   -> buffs   (buff kind, minus "Debuff" category)
--   EditBoxHET_DebuffSearch  -> debuffs (debuff kind + any "Debuff"-tagged
--                                        buff-file entry, e.g. Fire Breath)
--   EditBoxHET_AbilitySearch -> abilities
--   EditBoxHET_ItemSearch    -> items
-------------------------------------------------------------------------------
local addonName, ns = ...

local SearchEditBox = LibStub("AceGUI-3.0-Search-EditBox", true)
if not SearchEditBox then return end

local GetSpellName = C_Spell.GetSpellName
local GetSpellTexture = C_Spell.GetSpellTexture

-- Per-kind curated data lives in the SpellData_* files (single source of
-- truth). ns.SpellData.KINDS[kind] = { SPELLS, FLAT, BYID }. Sibling data
-- files may not all be present, so tolerate missing kinds.
local KINDS = ns.SpellData.KINDS or {}
local function KindFlat(kind) local k = KINDS[kind]; return (k and k.FLAT) or {} end
local function KindByID(kind) local k = KINDS[kind]; return (k and k.BYID) or {} end

-- Buff kind FLAT/BYID (backward-compatible names used below for the buff and
-- debuff predictors, which both consult the buff table's category tags).
local FLAT, BYID = ns.SpellData.FLAT, ns.SpellData.BYID

-- Colored class suffixes, built once.
local classNames
local function BuildClassNames()
    if classNames then return end
    classNames = { [""] = "" }
    for class, translation in pairs(LOCALIZED_CLASS_NAMES_MALE) do
        local c = RAID_CLASS_COLORS and RAID_CLASS_COLORS[class]
        if c then
            classNames[class] = (", |cff%.2x%.2x%.2x%s|r"):format(
                c.r * 255, c.g * 255, c.b * 255, translation)
        else
            classNames[class] = ", " .. translation
        end
    end
end

local function RowText(id, name, class, cat)
    local catText = cat and (", |cffffd100%s|r"):format(cat) or ""
    return ("|T%s:14|t %s%s%s |cff888888(%d)|r"):format(
        GetSpellTexture(id) or 134400, name,
        (class and classNames[class]) or "", catText, id)
end

-- Category scoping (buff/debuff predictors, both over the buff table): the
-- buff predictor offers every curated spell EXCEPT the "Debuff" category; the
-- debuff predictor offers ONLY that category. (Raw numeric IDs are always
-- accepted by every predictor — category only scopes name-based suggestions.)
local DEBUFF_CAT = "Debuff"
local function CatAllows(onlyDebuff, cat)
    if onlyDebuff then return cat == DEBUFF_CAT end
    return cat ~= DEBUFF_CAT
end

-- Each predictor searches an ordered list of "sources". A source is either:
--   { flat = <FLAT>, catFilter = <onlyDebuff bool or nil> }
-- catFilter nil  -> no category scoping (offer every entry in that flat)
-- catFilter false/true -> apply CatAllows(catFilter, cat) (buff/debuff split)
-- Sources are searched in order; the first name/number match wins.

-- First curated prefix match for a typed name across a predictor's sources
-- (shared by GetValues row fill and plain-Enter resolution).
local function FindFirstMatch(textLower, sources)
    for s = 1, #sources do
        local src = sources[s]
        local flat, cf = src.flat, src.catFilter
        for j = 1, #flat do
            local e = flat[j]
            if cf == nil or CatAllows(cf, e.cat) then
                local name = GetSpellName(e.id)
                if name and name:lower():find(textLower, 1, true) == 1 then
                    return e.id
                end
            end
        end
    end
end

-------------------------------------------------------------------------------
-- Restyle the lib's shared suggestion flyout (stock look = chunky
-- UI-DialogBox border + gold quest-log rows): flat dark panel, 1px HET-
-- blue border, solid highlight, white text. Done from OUR side instead
-- of editing the lib so it sticks even if another addon's copy of the
-- widget wins the version race. The shared frame is named
-- "AceGUI30SearchEditBox<num>Predictor" with the num of whichever
-- widget was created first — scan a small range; idempotent.
-------------------------------------------------------------------------------
local WHITE8X8 = "Interface\\Buttons\\WHITE8X8"
local function RestylePredictor()
    for i = 1, 30 do
        local f = _G["AceGUI30SearchEditBox" .. i .. "Predictor"]
        if f and not f._hetStyled and f.SetBackdrop then
            f._hetStyled = true
            f:SetBackdrop({
                bgFile = WHITE8X8,
                edgeFile = WHITE8X8,
                edgeSize = 1,
                insets = { left = 1, right = 1, top = 1, bottom = 1 },
            })
            f:SetBackdropColor(0.06, 0.06, 0.09, 0.97)
            f:SetBackdropBorderColor(0.07, 0.68, 0.91, 0.9) -- #11ace9
            if f.buttons then
                for _, btn in ipairs(f.buttons) do
                    btn:SetNormalFontObject(GameFontHighlightSmall)
                    btn:SetHighlightFontObject(GameFontHighlightSmall)
                    local hl = btn:GetHighlightTexture()
                    if hl then
                        hl:SetColorTexture(0.07, 0.68, 0.91, 0.25)
                        hl:ClearAllPoints()
                        hl:SetAllPoints(btn)
                        hl:SetAlpha(1)
                    end
                end
            end
        end
    end
end

-- Item name/texture lookup (item predictor). C_Item.GetItemInfo returns nil
-- for an item whose data the client hasn't cached yet (unlike spells, which
-- are always available) — so item name/number search silently finds nothing
-- until the data loads. We warm the cache: request every curated item ID on
-- init, and re-request on demand for any miss, so the next keystroke resolves.
-- The raw ID stays valid either way.
local GetItemName = C_Item and C_Item.GetItemInfo
local GetItemIcon = C_Item and C_Item.GetItemIconByID
local RequestItem = C_Item and C_Item.RequestLoadItemDataByID

-- Fire off async loads for every item ID in a flat list (idempotent; already-
-- cached IDs are a cheap no-op). Runs once per predictor init.
local function WarmItemCache(flat)
    if not (RequestItem and flat) then return end
    for j = 1, #flat do
        local id = flat[j].id
        if id and not GetItemName(id) then RequestItem(id) end
    end
end

local function ItemRowText(id, name)
    return ("|T%s:14|t %s |cff888888(%d)|r"):format(
        (GetItemIcon and GetItemIcon(id)) or 134400, name, id)
end

-------------------------------------------------------------------------------
-- Predictor registration
-------------------------------------------------------------------------------
-- One body, four predictors, each configured with:
--   cfg.sources  = ordered { {flat=, catFilter=}, ... } for name suggestions
--   cfg.byid     = by-id lookup for the numeric-entry live preview
--   cfg.isItem   = true for the item predictor (resolve names via C_Item and
--                  emit item: hyperlinks instead of spell:)
-- Registered names get an "EditBox" prefix by the lib, so the dialogControls
-- are EditBoxHET_SpellSearch / _DebuffSearch / _AbilitySearch / _ItemSearch.
local function MakePredictor(cfg)
    local sources, byid, isItem = cfg.sources, cfg.byid or {}, cfg.isItem
    local NameOf = isItem and GetItemName or GetSpellName
    return {
        Initialize = function()
            BuildClassNames()
            RestylePredictor()
            if isItem and sources[1] then WarmItemCache(sources[1].flat) end
        end,

        -- Fill the suggestion rows: result[key] = display text.
        GetValues = function(_, text, result, max)
            text = (text or ""):gsub("^%s+", ""):gsub("%s+$", "")
            if text == "" then return end
            local num = text:match("^%d+$") and tonumber(text)
            if num then
                -- Numeric entry: live preview of what that ID is (any valid
                -- ID, listed or not, regardless of category).
                local name = NameOf and NameOf(num)
                if name then
                    if isItem then
                        result[num] = ItemRowText(num, name)
                    else
                        local known = byid[num]
                        result[num] = RowText(num, name,
                            known and known.class, known and known.cat)
                    end
                elseif isItem and RequestItem then
                    -- uncached item ID: kick off the load so the next
                    -- keystroke resolves its name.
                    RequestItem(num)
                end
                return
            end
            local l = text:lower()
            local remaining = max or 15
            for s = 1, #sources do
                local src = sources[s]
                local flat, cf = src.flat, src.catFilter
                for j = 1, #flat do
                    local e = flat[j]
                    if result[e.id] == nil and (cf == nil or CatAllows(cf, e.cat)) then
                        local name = NameOf and NameOf(e.id)
                        if name and name:lower():find(l, 1, true) == 1 then
                            result[e.id] = isItem and ItemRowText(e.id, name)
                                or RowText(e.id, name, e.class, e.cat)
                            remaining = remaining - 1
                            if remaining <= 0 then return end
                        elseif isItem and not name and RequestItem then
                            RequestItem(e.id)  -- warm miss for next keystroke
                        end
                    end
                end
            end
        end,

        -- Validate/transform: row click passes its key; plain Enter passes
        -- key = nil (accept any valid ID, or resolve the first curated name
        -- match across this predictor's sources). Returning nil rejects.
        GetValue = function(_, text, key)
            if key then
                return key, tostring(key)
            end
            local num = tonumber(text)
            if num and num > 0 then
                num = math.floor(num)
                if (NameOf and NameOf(num)) then
                    return num, text
                end
                return
            end
            if text and #text >= 2 then
                local id = FindFirstMatch(text:lower(), sources)
                if id then
                    return id, tostring(id)
                end
            end
        end,

        -- Tooltip for the highlighted row.
        GetHyperlink = function(_, key)
            if type(key) == "number" then
                return (isItem and "item:" or "spell:") .. key
            end
        end,
    }
end

-- Buff predictor: buff table, excluding the "Debuff" category.
SearchEditBox:Register("HET_SpellSearch", MakePredictor({
    sources = { { flat = FLAT, catFilter = false } },
    byid = BYID,
}))
-- Debuff predictor: the debuff kind's own table PLUS any "Debuff"-tagged
-- entry that still lives in the buff file (e.g. Fire Breath).
SearchEditBox:Register("HET_DebuffSearch", MakePredictor({
    sources = {
        { flat = KindFlat("debuff") },        -- all entries (kind is debuff-only)
        { flat = FLAT, catFilter = true },    -- Debuff-tagged buff-file entries
    },
    byid = KindByID("debuff"),
}))
-- Ability predictor: the ability kind's table (cast spell IDs).
SearchEditBox:Register("HET_AbilitySearch", MakePredictor({
    sources = { { flat = KindFlat("ability") } },
    byid = KindByID("ability"),
}))
-- Item predictor: the item kind's table (item IDs), resolved via C_Item.
SearchEditBox:Register("HET_ItemSearch", MakePredictor({
    sources = { { flat = KindFlat("item") } },
    byid = KindByID("item"),
    isItem = true,
}))
