-------------------------------------------------------------------------------
-- TexturePicker.lua
-- Browser for the Texture element style. Opened from the element's options
-- via ns.OpenTexturePicker(value, kind, onSelect); calls onSelect(value, kind)
-- with kind "file" (FileDataID number or path string) or "atlas" (atlas name).
--
-- Three categories, only ONE of which ships data:
--   Blizzard Alerts  ns.TextureData (generated — see Tools/gen_texture_data.py)
--   Blizzard Atlas   C_Texture.GetAtlasElements() at runtime, ~15k entries, so
--                    it is SEARCH-GATED: nothing renders below MIN_ATLAS_SEARCH
--                    characters. Never render the unfiltered set.
--   Media            LibSharedMedia background + statusbar registrations.
--
-- Everything is lazy: the frame is built on first open, the atlas list is
-- fetched on first use of that category, and tiles come from a pool capped at
-- MAX_TILES. No events, no OnUpdate, nothing touched while closed.
--
-- Never opens in combat — the whole options panel is combat-locked, and the
-- preview regions this drives are forbidden while auras are secret.
-------------------------------------------------------------------------------
local addonName, ns = ...

local MAX_TILES         = 300  -- hard cap on rendered results
local MIN_ATLAS_SEARCH  = 3    -- atlas category stays blank below this
local TILE_W, TILE_H    = 100, 116
local COLS              = 6

local CAT_ALERTS = "Blizzard Alerts"
local CAT_ATLAS  = "Blizzard Atlas"
local CAT_MEDIA  = "Media"

local picker      -- the frame, built once
local atlasCache  -- sorted atlas name list, fetched once per session

-------------------------------------------------------------------------------
-- Data sources
-------------------------------------------------------------------------------
local function AtlasList()
    if atlasCache then return atlasCache end
    atlasCache = {}
    if C_Texture and C_Texture.GetAtlasElements then
        local ok, list = pcall(C_Texture.GetAtlasElements)
        if ok and type(list) == "table" then
            table.sort(list)
            atlasCache = list
        end
    end
    return atlasCache
end

local function LSM()
    return LibStub("LibSharedMedia-3.0", true)
end

-- Categories present in this session. Alerts only appears if the generated
-- data file actually loaded; Media only if LSM has something to offer.
local function Categories()
    local out = {}
    local data = ns.TextureData and ns.TextureData.CATEGORIES
    if data then
        local order = ns.TextureData.ORDER or {}
        for i = 1, #order do
            if data[order[i]] then out[#out + 1] = order[i] end
        end
    end
    if C_Texture and C_Texture.GetAtlasElements then
        out[#out + 1] = CAT_ATLAS
    end
    local lsm = LSM()
    if lsm then out[#out + 1] = CAT_MEDIA end
    return out
end

-- Build the filtered result set for a category. Each item is
-- { value = <number|string>, kind = "file"|"atlas", name = <display> }.
-- Returns items, truncated, gatedMessage.
local function BuildItems(category, filter)
    local items = {}
    filter = (filter or ""):lower()

    if category == CAT_ATLAS then
        if #filter < MIN_ATLAS_SEARCH then
            return items, false,
                ("Type at least %d characters to search %d atlases.")
                    :format(MIN_ATLAS_SEARCH, #AtlasList())
        end
        local list = AtlasList()
        for i = 1, #list do
            local name = list[i]
            if name:lower():find(filter, 1, true) then
                items[#items + 1] = { value = name, kind = "atlas", name = name }
                if #items >= MAX_TILES then
                    return items, true, nil
                end
            end
        end

    elseif category == CAT_MEDIA then
        local lsm = LSM()
        if lsm then
            local seen = {}
            for _, mt in ipairs({ "background", "statusbar" }) do
                local ok, names = pcall(lsm.List, lsm, mt)
                if ok and type(names) == "table" then
                    for i = 1, #names do
                        local key = names[i]
                        if not seen[key] and (filter == "" or key:lower():find(filter, 1, true)) then
                            seen[key] = true
                            local okPath, path = pcall(lsm.Fetch, lsm, mt, key)
                            if okPath and type(path) == "string" and path ~= "" then
                                items[#items + 1] = { value = path, kind = "file", name = key }
                            end
                        end
                    end
                end
            end
            table.sort(items, function(a, b) return a.name:lower() < b.name:lower() end)
        end

    else
        local data = ns.TextureData and ns.TextureData.CATEGORIES
        local cat = data and data[category]
        if type(cat) == "table" then
            for fdid, name in pairs(cat) do
                if filter == "" or name:lower():find(filter, 1, true) then
                    items[#items + 1] = { value = fdid, kind = "file", name = name }
                end
            end
            table.sort(items, function(a, b)
                if a.name == b.name then return a.value < b.value end
                return a.name:lower() < b.name:lower()
            end)
        end
    end

    return items, false, nil
end

-------------------------------------------------------------------------------
-- Apply a stored value to a texture region. The engine needs different calls
-- for atlases and files, which is why `kind` is stored alongside the value
-- rather than sniffed at render time.
-------------------------------------------------------------------------------
function ns.ApplyTextureValue(region, value, kind)
    if not region then return false end
    if value == nil or value == "" then
        region:SetTexture(nil)
        return false
    end
    if kind == "atlas" then
        -- SetAtlas fails silently on an unknown name; caller sees a blank region.
        return region:SetAtlas(value, false) ~= false
    end
    region:SetTexture(tonumber(value) or value)
    return true
end

-------------------------------------------------------------------------------
-- Frame
-------------------------------------------------------------------------------
local function EnsureTile(n)
    local tile = picker.tiles[n]
    if tile then return tile end

    tile = CreateFrame("Button", nil, picker.content, "BackdropTemplate")
    tile:SetSize(TILE_W - 4, TILE_H - 4)
    tile:SetBackdrop({
        bgFile   = "Interface\\Buttons\\WHITE8x8",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        edgeSize = 12,
        insets   = { left = 2, right = 2, top = 2, bottom = 2 },
    })
    tile:SetBackdropColor(0, 0, 0, 0.7)

    local tex = tile:CreateTexture(nil, "ARTWORK")
    tex:SetPoint("TOPLEFT", 5, -5)
    tex:SetPoint("BOTTOMRIGHT", -5, 19)
    tile.tex = tex

    local label = tile:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    label:SetPoint("BOTTOMLEFT", 3, 3)
    label:SetPoint("BOTTOMRIGHT", -3, 3)
    label:SetJustifyH("CENTER")
    label:SetMaxLines(1)
    tile.label = label

    tile:SetScript("OnEnter", function(self)
        self:SetBackdropBorderColor(1, 0.82, 0)
        if not self.item then return end
        GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
        GameTooltip:SetText(self.item.name)
        GameTooltip:AddLine(self.item.kind == "atlas"
            and ("atlas: " .. tostring(self.item.value))
            or  ("id: " .. tostring(self.item.value)), 0.8, 0.8, 0.8, true)
        GameTooltip:Show()
    end)
    tile:SetScript("OnLeave", function(self)
        self:SetBackdropBorderColor(1, 1, 1)
        GameTooltip:Hide()
    end)
    tile:SetScript("OnClick", function(self)
        if not self.item then return end
        picker.selected = self.item
        picker:UpdatePreview()
        -- Picking a texture closes the dialog — there is no OK/Cancel, so the
        -- click IS the confirmation.
        -- Capture the callback BEFORE hiding: the frame's OnHide clears
        -- picker.onSelect (see BuildPicker), so reading it after Hide() would
        -- get nil and the selection would silently do nothing. Fire it after
        -- the hide so the options panel refresh lands on an already-closed
        -- dialog rather than repainting behind it.
        local cb = picker.onSelect
        picker:Hide()
        if cb then cb(self.item.value, self.item.kind) end
    end)

    picker.tiles[n] = tile
    return tile
end

local function BuildPicker()
    if picker then return picker end

    local f = CreateFrame("Frame", "BuzzardAurasTexturePicker", UIParent,
                          "BasicFrameTemplateWithInset")
    f:SetSize(COLS * TILE_W + 56, 520)
    f:SetPoint("CENTER")
    f:SetFrameStrata("FULLSCREEN_DIALOG")
    f:SetToplevel(true)
    f:EnableMouse(true)
    f:SetMovable(true)
    f:RegisterForDrag("LeftButton")
    f:SetScript("OnDragStart", f.StartMoving)
    f:SetScript("OnDragStop", f.StopMovingOrSizing)
    f:SetScript("OnHide", function(self) self.onSelect = nil end)
    f:Hide()
    f.TitleText:SetText("Choose Texture")
    tinsert(UISpecialFrames, f:GetName())  -- Escape closes

    f.tiles = {}

    -- Preview + identity readout
    local preview = f:CreateTexture(nil, "ARTWORK")
    preview:SetSize(64, 64)
    preview:SetPoint("TOPLEFT", 16, -32)
    f.preview = preview

    local previewBg = f:CreateTexture(nil, "BACKGROUND")
    previewBg:SetAllPoints(preview)
    previewBg:SetColorTexture(0.18, 0.10, 0.18, 1)

    local idText = f:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    idText:SetPoint("TOPLEFT", preview, "TOPRIGHT", 12, -4)
    f.idText = idText

    local nameText = f:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    nameText:SetPoint("TOPLEFT", idText, "BOTTOMLEFT", 0, -4)
    f.nameText = nameText

    -- Category dropdown
    local drop = CreateFrame("Frame", "$parentCategory", f, "UIDropDownMenuTemplate")
    drop:SetPoint("TOPRIGHT", f, "TOPRIGHT", -8, -34)
    UIDropDownMenu_SetWidth(drop, 150)
    UIDropDownMenu_JustifyText(drop, "LEFT")
    f.drop = drop

    -- Search
    local search = CreateFrame("EditBox", nil, f, "SearchBoxTemplate")
    search:SetSize(180, 20)
    search:SetPoint("TOPRIGHT", drop, "BOTTOMRIGHT", -16, -2)
    search:SetAutoFocus(false)
    search:SetScript("OnTextChanged", function(self)
        SearchBoxTemplate_OnTextChanged(self)
        f.filter = self:GetText() or ""
        f:Refresh()
    end)
    search:SetScript("OnEscapePressed", function(self) self:SetText("") end)
    search:SetScript("OnEnterPressed", function(self) self:ClearFocus() end)
    f.search = search

    -- Results
    local scroll = CreateFrame("ScrollFrame", "$parentScroll", f,
                               "UIPanelScrollFrameTemplate")
    scroll:SetPoint("TOPLEFT", preview, "BOTTOMLEFT", 0, -12)
    scroll:SetPoint("BOTTOMRIGHT", f, "BOTTOMRIGHT", -32, 14)
    local content = CreateFrame("Frame", nil, scroll)
    content:SetSize(COLS * TILE_W, 10)
    scroll:SetScrollChild(content)
    f.scroll, f.content = scroll, content

    local note = f:CreateFontString(nil, "OVERLAY", "GameFontDisableLarge")
    note:SetPoint("CENTER", scroll, "CENTER", 0, 0)
    note:SetWidth(COLS * TILE_W - 40)
    note:SetJustifyH("CENTER")
    f.note = note

    function f:UpdatePreview()
        local sel = self.selected
        if not sel then
            self.preview:SetTexture(nil)
            self.idText:SetText("")
            self.nameText:SetText("")
            return
        end
        ns.ApplyTextureValue(self.preview, sel.value, sel.kind)
        self.idText:SetText(sel.name or "")
        self.nameText:SetText(sel.kind == "atlas"
            and ("atlas: " .. tostring(sel.value))
            or  ("id: " .. tostring(sel.value)))
    end

    function f:Refresh()
        local items, truncated, gated = BuildItems(self.category, self.filter)

        for i = 1, #self.tiles do self.tiles[i]:Hide() end

        if gated then
            self.note:SetText(gated)
            self.content:SetHeight(10)
            return
        end
        if #items == 0 then
            self.note:SetText("No matches.")
            self.content:SetHeight(10)
            return
        end
        self.note:SetText("")

        for i = 1, #items do
            local tile = EnsureTile(i)
            local item = items[i]
            tile.item = item
            ns.ApplyTextureValue(tile.tex, item.value, item.kind)
            tile.label:SetText(item.name)
            tile:ClearAllPoints()
            tile:SetPoint("TOPLEFT", self.content, "TOPLEFT",
                          ((i - 1) % COLS) * TILE_W + 2,
                          -math.floor((i - 1) / COLS) * TILE_H - 2)
            tile:Show()
        end

        self.content:SetHeight(math.max(10, math.ceil(#items / COLS) * TILE_H + 4))
        if truncated then
            self.TitleText:SetText(
                ("Choose Texture — first %d matches, refine your search"):format(MAX_TILES))
        else
            self.TitleText:SetText(("Choose Texture — %d"):format(#items))
        end
    end

    function f:SetCategory(cat)
        self.category = cat
        UIDropDownMenu_SetText(self.drop, cat or "")
        self:Refresh()
    end

    UIDropDownMenu_Initialize(drop, function()
        local info = UIDropDownMenu_CreateInfo()
        info.func = function(btn) f:SetCategory(btn.value) end
        for _, cat in ipairs(Categories()) do
            info.text, info.value = cat, cat
            info.checked = (cat == f.category)
            UIDropDownMenu_AddButton(info)
        end
    end)

    picker = f
    return f
end

-------------------------------------------------------------------------------
-- Public entry point
-------------------------------------------------------------------------------
function ns.OpenTexturePicker(value, kind, onSelect)
    if InCombatLockdown() then
        print("|cffff8c14Buzzard Auras|r Texture picker is unavailable in combat.")
        return
    end

    local f = BuildPicker()
    f.onSelect = onSelect
    f.filter = ""
    if f.search then f.search:SetText("") end

    f.selected = (value ~= nil and value ~= "")
        and { value = value, kind = kind or "file", name = tostring(value) }
        or nil

    -- Open on the category the current value most likely came from.
    local cats = Categories()
    local want = (kind == "atlas") and CAT_ATLAS or (cats[1] or CAT_ATLAS)
    f:SetCategory(want)
    f:UpdatePreview()
    f:Show()
    f:Raise()
end

function ns.CloseTexturePicker()
    if picker then picker:Hide() end
end
