-- EmoteMenuUI.lua
-- UI, rendering, interactions for EmoteMenu. Core lives in EmoteMenu.lua

local ADDON_NAME, EmoteMenu = ...
local DB = EmoteMenu.DB

-- UI constants/state
local SHOW_ALL_INDEX = 0
local FAVORITES_INDEX = -1
local SHOW_CATEGORY_SIDEBAR = false
local FRAME_WIDTH_WITH_SIDEBAR = 600
local FRAME_WIDTH_NO_SIDEBAR = 430
local FRAME_HEIGHT = 620
local ICONS_PATH = "Interface/AddOns/EmoteMenu/Icons/"

-- Filters (persisted)
local filterHasSound, filterHasAnim, searchText = false, false, ""

-- Default selection to "All" unless already set (persist across reloads if UI retains state)
EmoteMenu.selectedCategory = EmoteMenu.selectedCategory or SHOW_ALL_INDEX

-- UI settings (persisted): sidebar toggle
local function LoadUISettings()
    EmoteMenuSaved = EmoteMenuSaved or {}
    if EmoteMenuSaved.showSidebar ~= nil then
        SHOW_CATEGORY_SIDEBAR = EmoteMenuSaved.showSidebar and true or false
    end
end

local function SaveUISettings()
    EmoteMenuSaved = EmoteMenuSaved or {}
    EmoteMenuSaved.showSidebar = SHOW_CATEGORY_SIDEBAR and true or false
end

local function LoadFilters()
    EmoteMenuSaved = EmoteMenuSaved or {}
    local f = EmoteMenuSaved.filters or {}
    filterHasSound = not not f.sound
    filterHasAnim = not not f.anim
    searchText = f.search or ""
end

local function SaveFilters()
    EmoteMenuSaved = EmoteMenuSaved or {}
    EmoteMenuSaved.filters = { sound = filterHasSound, anim = filterHasAnim, search = searchText }
end

local function matchesFilter(info)
    if filterHasSound and not info.sound then return false end
    if filterHasAnim and not info.animation then return false end
    if searchText and searchText ~= "" then
        local q = string.lower(searchText)
        local lbl = string.lower(tostring(info.label or ""))
        local cmd = string.lower(tostring(info.cmd or ""))
        if not string.find(lbl, q, 1, true) and not string.find(cmd, q, 1, true) then
            return false
        end
    end
    return true
end

-- Icon helpers
local TrySetIcon = function(texture, cmd)
    if not texture or not cmd then return false end
    cmd = cmd:lower()
    local ok = texture:SetTexture(ICONS_PATH .. cmd .. ".blp")
    if not ok then ok = texture:SetTexture(ICONS_PATH .. cmd .. ".tga") end
    return ok and true or false
end

local function CreateBackdrop(frame)
    if frame.SetBackdrop then
        frame:SetBackdrop({
            bgFile = "Interface/QuestionFrame/question-background",
            edgeFile = "Interface/Tooltips/UI-Tooltip-Border",
            tile = true, tileSize = 256, edgeSize = 16,
            insets = { left = 4, right = 4, top = 4, bottom = 4 }
        })
        if frame.SetBackdropColor then frame:SetBackdropColor(0.2, 0.2, 0.2, 0.90) end
        if frame.SetBackdropBorderColor then frame:SetBackdropBorderColor(0.8, 0.8, 0.8, 1) end
    end
end

local function SetDrag(frame)
    frame:EnableMouse(true)
    frame:SetMovable(true)
    frame:RegisterForDrag("LeftButton")
    frame:SetScript("OnDragStart", frame.StartMoving)
    frame:SetScript("OnDragStop", function(self)
        self:StopMovingOrSizing()
        self._moved = true
    end)
end

-- Drag ghost
local dragGhost
local function EnsureDragGhost()
    if dragGhost then return dragGhost end
    local f = CreateFrame("Frame", nil, UIParent)
    f:SetFrameStrata("TOOLTIP")
    f:SetSize(36, 36)
    f:EnableMouse(false)
    f:Hide()
    f.icon = f:CreateTexture(nil, "ARTWORK")
    f.icon:SetAllPoints(true)
    f.icon:SetTexCoord(0.07, 0.93, 0.07, 0.93)
    dragGhost = f
    return dragGhost
end

local function ShowDragGhost(cmd)
    local f = EnsureDragGhost()
    if not TrySetIcon(f.icon, cmd) then
        f.icon:SetTexture("Interface/Icons/INV_Misc_QuestionMark")
        f.icon:SetTexCoord(0.07, 0.93, 0.07, 0.93)
    end
    f:Show()
    f:SetScript("OnUpdate", function(self)
        local x, y = GetCursorPosition()
        local s = UIParent:GetEffectiveScale()
        x, y = x / s, y / s
        self:ClearAllPoints()
        self:SetPoint("CENTER", UIParent, "BOTTOMLEFT", x, y)
    end)
end

local function HideDragGhost()
    if not dragGhost then return end
    dragGhost:SetScript("OnUpdate", nil)
    dragGhost:Hide()
end

-- UI locals
local mainFrame, rightPanelRef, categoryContent, emoteScroll, emoteContent
local categoryButtons, emoteButtons, sectionHeaders = {}, {}, {}

-- Public access for UI toggle
EmoteMenu.Toggle = EmoteMenu.Toggle -- placeholder for assignment later

-- Launcher
local function CreateLauncherButton()
    if _G.EmoteMenuLauncherButton then return end
    local anchor = _G.ChatFrameMenuButton
    local btn = CreateFrame("Button", "EmoteMenuLauncherButton", UIParent)
    btn:SetSize(26, 26)
    if anchor then
        btn:SetPoint("BOTTOM", anchor, "TOP", 0, 6)
        btn:SetFrameStrata("HIGH")
    else
        if _G.ChatFrame1 then
            btn:SetPoint("BOTTOMLEFT", _G.ChatFrame1, "TOPLEFT", 0, 6)
        else
            btn:SetPoint("BOTTOMLEFT", UIParent, "BOTTOMLEFT", 10, 140)
        end
    end
    btn.icon = btn:CreateTexture(nil, "ARTWORK")
    btn.icon:SetAllPoints(true)
    if not TrySetIcon(btn.icon, "emotemenubutton") then
        btn.icon:SetTexture("Interface/Icons/INV_Misc_QuestionMark")
        btn.icon:SetTexCoord(0.07, 0.93, 0.07, 0.93)
    end
    btn:SetHighlightTexture("Interface/Buttons/ButtonHilight-Square", "ADD")
    btn:RegisterForClicks("LeftButtonUp")
    btn:SetScript("OnClick", function()
        if EmoteMenu.Toggle then EmoteMenu.Toggle() end
    end)
    btn:SetScript("OnEnter", function(self)
        GameTooltip:SetOwner(self, "ANCHOR_TOP")
        GameTooltip:SetText("Emote Menu")
        GameTooltip:Show()
    end)
    btn:SetScript("OnLeave", function()
        if GameTooltip:IsShown() then GameTooltip:Hide() end
    end)
    btn:Show()
end

-- Categories panel
local function RenderCategories(selectedCategory)
    for _, b in ipairs(categoryButtons) do b:Hide() end
    wipe(categoryButtons)
    local y = -8
    do
        local btn = CreateFrame("Button", nil, categoryContent, "UIPanelButtonTemplate")
        btn:SetSize(140, 24)
        btn:SetPoint("TOPLEFT", 8, y)
        btn:SetText("All")
        btn:SetScript("OnClick", function()
            EmoteMenu.selectedCategory = SHOW_ALL_INDEX
            RenderCategories(EmoteMenu.selectedCategory)
            if mainFrame and mainFrame.RenderEmotes then mainFrame.RenderEmotes() end
        end)
        if selectedCategory == SHOW_ALL_INDEX then btn:Disable() else btn:Enable() end
        table.insert(categoryButtons, btn)
        y = y - 28
    end
    do
        local btn = CreateFrame("Button", nil, categoryContent, "UIPanelButtonTemplate")
        btn:SetSize(140, 24)
        btn:SetPoint("TOPLEFT", 8, y)
        btn:SetText("Favorites")
        btn:SetScript("OnClick", function()
            EmoteMenu.selectedCategory = FAVORITES_INDEX
            RenderCategories(EmoteMenu.selectedCategory)
            if mainFrame and mainFrame.RenderEmotes then mainFrame.RenderEmotes() end
        end)
        if selectedCategory == FAVORITES_INDEX then btn:Disable() else btn:Enable() end
        table.insert(categoryButtons, btn)
        y = y - 28
    end
    for i, cat in ipairs(DB.categories or {}) do
        local btn = CreateFrame("Button", nil, categoryContent, "UIPanelButtonTemplate")
        btn:SetSize(140, 24)
        btn:SetPoint("TOPLEFT", 8, y)
        btn:SetText(cat.name or ("Category %d"):format(i))
        btn:SetScript("OnClick", function()
            EmoteMenu.selectedCategory = i
            RenderCategories(EmoteMenu.selectedCategory)
            if mainFrame and mainFrame.RenderEmotes then mainFrame.RenderEmotes() end
        end)
        if i == selectedCategory then btn:Disable() else btn:Enable() end
        table.insert(categoryButtons, btn)
        y = y - 28
    end
end

-- Helpers
local function getCategoryEmotes()
    if EmoteMenu.selectedCategory == FAVORITES_INDEX then
        local list = {}
        for _, cat in ipairs(DB.categories or {}) do
            for _, e in ipairs(cat.emotes or {}) do
                if EmoteMenu.favoritesSet[e.cmd] then table.insert(list, e) end
            end
        end
        return list
    end
    local cat = (DB.categories and DB.categories[EmoteMenu.selectedCategory])
    if not cat then return {} end
    return cat.emotes or {}
end

local emoteButtonsCache = {}
local function CreateOrGetEmoteButton(index)
    local btn = emoteButtons[index]
    if not btn then
        btn = CreateFrame("Button", nil, emoteContent, "BackdropTemplate")
        btn:SetFrameStrata("DIALOG")
        btn:SetSize(100, 28)
        CreateBackdrop(btn)
        btn.icon = btn:CreateTexture(nil, "ARTWORK")
        btn.icon:SetSize(32, 32)
        btn.icon:SetPoint("LEFT", 4, 0)
        btn.label = btn:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        btn.label:SetPoint("LEFT", btn.icon, "RIGHT", 6, 0)
        btn.label:SetPoint("RIGHT", -6, 0)
        btn.label:SetJustifyH("LEFT")
        btn:SetScript("OnEnter", function(self)
            if not self.tooltipBase then return end
            local cmd = self._cmd
            local lines = { self.tooltipBase }
            if cmd and EmoteMenu.hiddenSet[cmd] then
                table.insert(lines, "|cffaaaaaaAlt-Click to unhide|r")
            else
                table.insert(lines, "|cffaaaaaaAlt-Click to hide|r")
            end
            table.insert(lines, "|cffaaaaaaRight-Click to toggle favorite|r")
            GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
            GameTooltip:SetText(table.concat(lines, "\n"))
            GameTooltip:Show()
        end)
        btn:SetScript("OnLeave", function()
            if GameTooltip:IsOwned(btn) then GameTooltip:Hide() end
        end)
        emoteButtons[index] = btn
    end
    btn:Show()
    return btn
end

local function mainFrame_RenderEmotes()
    for _, b in ipairs(emoteButtons) do b:Hide() end
    for _, h in ipairs(sectionHeaders) do h:Hide() end

    local spacingX, spacingY = 4, 4
    local startX, startY = 8, -8
    local btnW, btnH = 38, 38
    local panelWidth = rightPanelRef and rightPanelRef:GetWidth() or (SHOW_CATEGORY_SIDEBAR and 600 or 430) - 20
    local cols = math.max(3, math.floor((panelWidth - startX) / (btnW + spacingX)))

    local row, col = 0, 0
    local btnIndex = 0
    local gridIndexInfo = {}
    local currentSection = nil
    local currentCatName = nil

    local function placeHeader(text)
        local idx
        for i, h in ipairs(sectionHeaders) do if not h:IsShown() then idx = i; break end end
        local hdr = idx and sectionHeaders[idx] or emoteContent:CreateFontString(nil, "OVERLAY", "GameFontHighlightLarge")
        if not idx then table.insert(sectionHeaders, hdr) end
        hdr:Show()
        hdr:ClearAllPoints()
        hdr:SetPoint("TOPLEFT", startX, startY - row * (btnH + spacingY) + 2)
        hdr:SetText(text)
        local headerGapRows = 0.35
        row = row + headerGapRows
        col = 0
    end

    local function placeButton(info)
        btnIndex = btnIndex + 1
        local btn = CreateOrGetEmoteButton(btnIndex)
        btn:SetSize(btnW, btnH)
        local x = startX + col * (btnW + spacingX)
        local y = startY - row * (btnH + spacingY)
        btn:ClearAllPoints()
        btn:SetPoint("TOPLEFT", x, y)
        local labelText = info.label or info.cmd
        btn.tooltipBase = labelText
        btn._cmd = info.cmd
        btn._section = currentSection
        btn._catName = currentCatName
        btn._gridIndex = btnIndex
        gridIndexInfo[btnIndex] = { section = currentSection, cat = currentCatName, cmd = info.cmd, info = info }
        local hasIcon = TrySetIcon(btn.icon, info.cmd)
        if not hasIcon then
            btn.icon:SetTexture(nil)
            btn.label:Show(); btn.label:SetText(labelText)
            btn.label:ClearAllPoints(); btn.label:SetPoint("CENTER")
        else
            btn.label:SetText(""); btn.label:Hide()
        end
        if EmoteMenu.favoritesSet[info.cmd] then
            if btn.SetBackdropBorderColor then btn:SetBackdropBorderColor(1, 0.84, 0, 1) end
        else
            if btn.SetBackdropBorderColor then btn:SetBackdropBorderColor(0.6, 0.6, 0.6, 1) end
        end
        local isHidden = EmoteMenu.hiddenSet[info.cmd] and true or false
        if isHidden and IsAltKeyDown() then
            btn:SetAlpha(0.35)
            if btn.icon then btn.icon:SetDesaturated(true) end
            if btn.SetBackdropBorderColor then btn:SetBackdropBorderColor(0.35, 0.35, 0.35, 1) end
        else
            btn:SetAlpha(1)
            if btn.icon then btn.icon:SetDesaturated(false) end
        end
        btn:RegisterForClicks("LeftButtonUp", "RightButtonUp")
        btn:SetScript("OnClick", function(self, mouseButton)
            if mouseButton == "RightButton" then
                if EmoteMenu.favoritesSet[info.cmd] then
                    EmoteMenu.favoritesSet[info.cmd] = nil
                    for i = #EmoteMenu.favorites, 1, -1 do if EmoteMenu.favorites[i] == info.cmd then table.remove(EmoteMenu.favorites, i) end end
                else
                    EmoteMenu.favoritesSet[info.cmd] = true
                    table.insert(EmoteMenu.favorites, info.cmd)
                end
                EmoteMenu.SaveFavorites()
                mainFrame.RenderEmotes(); RenderCategories(EmoteMenu.selectedCategory)
                return
            end
            if IsAltKeyDown() then
                if EmoteMenu.hiddenSet[info.cmd] then
                    EmoteMenu.hiddenSet[info.cmd] = nil
                    for i = #EmoteMenu.hidden, 1, -1 do if EmoteMenu.hidden[i] == info.cmd then table.remove(EmoteMenu.hidden, i) end end
                else
                    EmoteMenu.hiddenSet[info.cmd] = true
                    table.insert(EmoteMenu.hidden, info.cmd)
                end
                EmoteMenu.SaveHidden(); mainFrame.RenderEmotes()
            else
                EmoteMenu.RunEmote(info.cmd)
            end
        end)
        btn:EnableMouse(true); btn:RegisterForDrag("LeftButton"); btn:SetMovable(true)
        btn:SetScript("OnDragStart", function(self)
            if IsAltKeyDown() then return end
            self._dragging = true; self._dragSourceIndex = self._gridIndex; self:SetAlpha(0.6)
            if self._cmd then ShowDragGhost(self._cmd) end
        end)
        btn:SetScript("OnDragStop", function(self)
            if not self._dragging then return end
            self._dragging = false; self:SetAlpha(1); HideDragGhost()
            local cx, cy = GetCursorPosition(); local uiScale = UIParent:GetEffectiveScale(); cx, cy = cx / uiScale, cy / uiScale
            local dropIndex
            for i = 1, btnIndex do
                local b = emoteButtons[i]
                if b and b:IsShown() then
                    local left, bottom, width, height = b:GetLeft(), b:GetBottom(), b:GetWidth(), b:GetHeight()
                    if left and bottom and width and height then
                        local right, top = left + width, bottom + height
                        if cx >= left and cx <= right and cy >= bottom and cy <= top then dropIndex = i; break end
                    end
                end
            end
            if not dropIndex then mainFrame.RenderEmotes(); return end
            local src, dst = gridIndexInfo[self._dragSourceIndex], gridIndexInfo[dropIndex]
            if not src or not dst then mainFrame.RenderEmotes(); return end
            if src.section ~= dst.section then mainFrame.RenderEmotes(); return end
            if src.section == "category" and src.cat ~= dst.cat then mainFrame.RenderEmotes(); return end
            if src.section == "favorites" then
                local srcCmd, dstCmd = src.cmd, dst.cmd
                local srcPos, dstPos
                for i, c in ipairs(EmoteMenu.favorites) do
                    if c == srcCmd then srcPos = i end
                    if c == dstCmd then dstPos = i end
                end
                if not srcPos or not dstPos or srcPos == dstPos then mainFrame.RenderEmotes(); return end
                local moving = table.remove(EmoteMenu.favorites, srcPos)
                if dstPos > srcPos then dstPos = dstPos - 1 end
                table.insert(EmoteMenu.favorites, dstPos, moving)
                EmoteMenu.SaveFavorites(); mainFrame.RenderEmotes(); return
            elseif src.section == "category" then
                local cat
                for _, c in ipairs(DB.categories or {}) do if c.name == src.cat then cat = c; break end end
                if not cat or not cat.emotes then mainFrame.RenderEmotes(); return end
                local rendered = {}
                for _, e in ipairs(cat.emotes) do
                    if (not EmoteMenu.hiddenSet[e.cmd] or IsAltKeyDown()) and matchesFilter(e) then table.insert(rendered, e) end
                end
                local srcEmote, dstEmote
                for _, item in ipairs(rendered) do if item.cmd == src.cmd then srcEmote = item end; if item.cmd == dst.cmd then dstEmote = item end end
                if not srcEmote or not dstEmote then mainFrame.RenderEmotes(); return end
                local posSrc, posDst
                for i, e in ipairs(cat.emotes) do if e == srcEmote then posSrc = i end; if e == dstEmote then posDst = i end end
                if not posSrc or not posDst or posSrc == posDst then mainFrame.RenderEmotes(); return end
                local moving = table.remove(cat.emotes, posSrc)
                if posDst > posSrc then posDst = posDst - 1 end
                table.insert(cat.emotes, posDst, moving)
                EmoteMenu.SaveOrderForCategory(cat.name, cat); mainFrame.RenderEmotes(); return
            end
        end)
        col = col + 1; if col >= cols then col = 0; row = row + 1 end
    end

    local function tryPlaceButton(info)
        if EmoteMenu.hiddenSet[info.cmd] and not IsAltKeyDown() then return false end
        if not matchesFilter(info) then return false end
        placeButton(info); return true
    end

    local function sectionHasMatches(list, excludeFavorites)
        for _, info in ipairs(list or {}) do
            if (not excludeFavorites or not EmoteMenu.favoritesSet[info.cmd]) and matchesFilter(info) then return true end
        end
        return false
    end

    if EmoteMenu.selectedCategory == SHOW_ALL_INDEX then
        if #EmoteMenu.favorites > 0 then
            local favHasMatches = false
            for _, cmd in ipairs(EmoteMenu.favorites) do
                for _, cat in ipairs(DB.categories or {}) do
                    for _, info in ipairs(cat.emotes or {}) do
                        if info.cmd == cmd and matchesFilter(info) and (not EmoteMenu.hiddenSet[info.cmd] or IsAltKeyDown()) then favHasMatches = true; break end
                    end
                    if favHasMatches then break end
                end
                if favHasMatches then break end
            end
            if favHasMatches then
                placeHeader("Favorites"); currentSection = "favorites"; currentCatName = nil
                for _, cmd in ipairs(EmoteMenu.favorites) do
                    for _, cat in ipairs(DB.categories or {}) do
                        for _, info in ipairs(cat.emotes or {}) do
                            if info.cmd == cmd then if tryPlaceButton(info) then break end end
                        end
                    end
                end
                if col > 0 then col = 0; row = row + 1 end
            end
        end
        for i, cat in ipairs(DB.categories or {}) do
            if cat.emotes and #cat.emotes > 0 and sectionHasMatches(cat.emotes, true) then
                placeHeader(cat.name or ("Category %d"):format(i)); currentSection = "category"; currentCatName = cat.name
                for _, info in ipairs(cat.emotes) do
                    if not EmoteMenu.favoritesSet[info.cmd] then tryPlaceButton(info) end
                end
                if col > 0 then col = 0; row = row + 1 end
            end
        end
    elseif EmoteMenu.selectedCategory == FAVORITES_INDEX then
        if #EmoteMenu.favorites == 0 then
            placeHeader("Favorites");
            local fs = emoteContent:CreateFontString(nil, "OVERLAY", "GameFontDisable")
            fs:SetPoint("TOPLEFT", startX, startY - row * (btnH + spacingY))
            fs:SetText("No favorites yet. Right-click an emote to add it.")
            table.insert(sectionHeaders, fs)
        else
            currentSection = "favorites"; currentCatName = nil
            local placed = 0
            for _, cmd in ipairs(EmoteMenu.favorites) do
                for _, cat in ipairs(DB.categories or {}) do
                    for _, info in ipairs(cat.emotes or {}) do
                        if info.cmd == cmd and matchesFilter(info) and (not EmoteMenu.hiddenSet[info.cmd] or IsAltKeyDown()) then
                            placeButton(info); placed = placed + 1; break
                        end
                    end
                end
            end
            if placed == 0 then
                local fs = emoteContent:CreateFontString(nil, "OVERLAY", "GameFontDisable")
                fs:SetPoint("TOPLEFT", startX, startY)
                fs:SetText("No favorites match filters.")
                table.insert(sectionHeaders, fs)
            end
        end
    else
        currentSection = "category"; currentCatName = (DB.categories and DB.categories[EmoteMenu.selectedCategory] and DB.categories[EmoteMenu.selectedCategory].name) or nil
        local emotes = getCategoryEmotes()
        local placed = 0
        for _, info in ipairs(emotes) do if tryPlaceButton(info) then placed = placed + 1 end end
        if placed == 0 then
            local fs = emoteContent:CreateFontString(nil, "OVERLAY", "GameFontDisable")
            fs:SetPoint("TOPLEFT", startX, startY)
            fs:SetText("No emotes match filters.")
            table.insert(sectionHeaders, fs)
        end
    end

    local totalRows = math.ceil(row + (col > 0 and 1 or 0))
    local contentW = math.max(1, (cols * btnW) + ((cols - 1) * spacingX) + 16)
    local contentH = math.max(1, totalRows * (btnH + spacingY) + 16)
    emoteContent:SetSize(contentW, contentH)
end

local function CreateUI()
    if mainFrame then return end
    mainFrame = CreateFrame("Frame", "EmoteMenuMainFrame", UIParent, "BackdropTemplate")
    -- Load persisted UI settings (e.g., sidebar visibility) before sizing
    LoadUISettings()
    local frameWidth = SHOW_CATEGORY_SIDEBAR and FRAME_WIDTH_WITH_SIDEBAR or FRAME_WIDTH_NO_SIDEBAR
    mainFrame:SetSize(frameWidth, FRAME_HEIGHT)
    mainFrame:SetPoint("CENTER")
    mainFrame:SetFrameStrata("DIALOG")
    mainFrame:SetToplevel(true)
    CreateBackdrop(mainFrame)
    SetDrag(mainFrame)
    mainFrame:Hide()
    mainFrame:SetScript("OnHide", function() HideDragGhost() end)
    table.insert(UISpecialFrames, "EmoteMenuMainFrame")

    -- Sidebar toggle button (to the left of the title)
    local sidebarBtn = CreateFrame("Button", nil, mainFrame, "UIPanelButtonTemplate")
    sidebarBtn:SetSize(22, 22)
    sidebarBtn:SetPoint("TOPLEFT", 10, -10)
    sidebarBtn:SetText(SHOW_CATEGORY_SIDEBAR and ">>" or "<<")

    local title = mainFrame:CreateFontString(nil, "OVERLAY", "GameFontHighlightLarge")
    title:ClearAllPoints()
    title:SetPoint("LEFT", sidebarBtn, "RIGHT", 6, 0)
    title:SetPoint("TOP", 0, -12)
    title:SetText("Emote Menu")

    local close = CreateFrame("Button", nil, mainFrame, "UIPanelCloseButton")
    close:SetPoint("TOPRIGHT", 0, 0)

    LoadFilters()

    local soundCheck = CreateFrame("CheckButton", "EmoteMenuSoundFilter", mainFrame, "UICheckButtonTemplate")
    soundCheck:SetPoint("TOPLEFT", title, "TOPRIGHT", 16, 2)
    if soundCheck.Text then soundCheck.Text:SetText(""); soundCheck.Text:Hide() end
    local soundIcon = soundCheck:CreateTexture(nil, "ARTWORK")
    soundIcon:SetSize(16, 16); soundIcon:SetPoint("LEFT", soundCheck, "RIGHT", 2, 0)
    soundIcon:SetTexture(ICONS_PATH .. "shout.tga")
    soundCheck:SetChecked(filterHasSound)
    soundCheck:SetScript("OnEnter", function(self) GameTooltip:SetOwner(self, "ANCHOR_TOP"); GameTooltip:SetText("Only show emotes that play a voice line"); GameTooltip:Show() end)
    soundCheck:SetScript("OnLeave", function() if GameTooltip:IsShown() then GameTooltip:Hide() end end)
    soundCheck:SetScript("OnClick", function(self) filterHasSound = self:GetChecked() and true or false; SaveFilters(); mainFrame.RenderEmotes() end)

    local animCheck = CreateFrame("CheckButton", "EmoteMenuAnimFilter", mainFrame, "UICheckButtonTemplate")
    animCheck:SetPoint("LEFT", soundIcon, "RIGHT", 16, 0)
    if animCheck.Text then animCheck.Text:SetText(""); animCheck.Text:Hide() end
    local animIcon = animCheck:CreateTexture(nil, "ARTWORK")
    animIcon:SetSize(16, 16); animIcon:SetPoint("LEFT", animCheck, "RIGHT", 2, 0)
    animIcon:SetTexture(ICONS_PATH .. "dance.tga")
    animCheck:SetChecked(filterHasAnim)
    animCheck:SetScript("OnEnter", function(self) GameTooltip:SetOwner(self, "ANCHOR_TOP"); GameTooltip:SetText("Only show emotes that have an animation"); GameTooltip:Show() end)
    animCheck:SetScript("OnLeave", function() if GameTooltip:IsShown() then GameTooltip:Hide() end end)
    animCheck:SetScript("OnClick", function(self) filterHasAnim = self:GetChecked() and true or false; SaveFilters(); mainFrame.RenderEmotes() end)

    local searchBox = CreateFrame("EditBox", "EmoteMenuSearchBox", mainFrame, "SearchBoxTemplate")
    searchBox:SetSize(120, 24); searchBox:SetAutoFocus(false); searchBox:SetPoint("LEFT", animIcon, "RIGHT", 16, 0)
    if searchText and searchText ~= "" then searchBox:SetText(searchText); if type(SearchBoxTemplate_OnEditFocusGained) == "function" then SearchBoxTemplate_OnEditFocusLost(searchBox) end end
    searchBox:SetScript("OnTextChanged", function(self) if type(SearchBoxTemplate_OnTextChanged) == "function" then SearchBoxTemplate_OnTextChanged(self) end; searchText = self:GetText() or ""; SaveFilters(); mainFrame.RenderEmotes() end)
    searchBox:SetScript("OnEnterPressed", function(self) self:ClearFocus() end)
    searchBox:SetScript("OnEscapePressed", function(self) self:SetText(""); searchText = ""; SaveFilters(); self:ClearFocus(); mainFrame.RenderEmotes() end)

    local leftPanel = CreateFrame("Frame", nil, mainFrame, "BackdropTemplate")
    leftPanel:SetFrameStrata("DIALOG"); leftPanel:SetPoint("TOPLEFT", 10, -40); leftPanel:SetPoint("BOTTOMLEFT", 10, 10); leftPanel:SetWidth(160)
    CreateBackdrop(leftPanel)

    categoryContent = CreateFrame("Frame", nil, leftPanel)
    categoryContent:SetPoint("TOPLEFT", 4, -4); categoryContent:SetPoint("BOTTOMRIGHT", -4, 4)

    local rightPanel = CreateFrame("Frame", nil, mainFrame, "BackdropTemplate")
    rightPanel:SetFrameStrata("DIALOG")
    rightPanel:SetPoint("BOTTOMRIGHT", -10, 10); CreateBackdrop(rightPanel); rightPanelRef = rightPanel

    emoteScroll = CreateFrame("ScrollFrame", nil, rightPanel, "UIPanelScrollFrameTemplate")
    emoteScroll:SetFrameStrata("DIALOG"); emoteScroll:SetPoint("TOPLEFT", 4, -4); emoteScroll:SetPoint("BOTTOMRIGHT", -26, 4)
    emoteContent = CreateFrame("Frame", nil, emoteScroll); emoteContent:SetSize(1, 1); emoteScroll:SetScrollChild(emoteContent)

    mainFrame.RenderEmotes = mainFrame_RenderEmotes

    EmoteMenu.LoadFavorites(); EmoteMenu.LoadHidden(); EmoteMenu.LoadOrder(); EmoteMenu.BuildOtherCategory(); EmoteMenu.ApplyOrderToAllCategories()

    -- Apply initial sidebar layout and wire toggle
    local function ApplySidebarLayout()
        if SHOW_CATEGORY_SIDEBAR then
            mainFrame:SetWidth(FRAME_WIDTH_WITH_SIDEBAR)
            leftPanel:Show()
            rightPanel:ClearAllPoints()
            rightPanel:SetPoint("TOPLEFT", leftPanel, "TOPRIGHT", 10, 0)
            rightPanel:SetPoint("BOTTOMRIGHT", -10, 10)
            RenderCategories(EmoteMenu.selectedCategory)
            sidebarBtn:SetText("<<")
        else
            mainFrame:SetWidth(FRAME_WIDTH_NO_SIDEBAR)
            leftPanel:Hide()
            rightPanel:ClearAllPoints()
            rightPanel:SetPoint("TOPLEFT", 10, -40)
            rightPanel:SetPoint("BOTTOMRIGHT", -10, 10)
            sidebarBtn:SetText(">>")
        end
        SaveUISettings()
        mainFrame.RenderEmotes()
    end

    sidebarBtn:SetScript("OnClick", function()
        SHOW_CATEGORY_SIDEBAR = not SHOW_CATEGORY_SIDEBAR
        ApplySidebarLayout()
    end)

    ApplySidebarLayout()

    mainFrame:RegisterEvent("MODIFIER_STATE_CHANGED")
    mainFrame:SetScript("OnEvent", function(_, event) if event == "MODIFIER_STATE_CHANGED" then mainFrame.RenderEmotes() end end)
end

local function PositionMenuDefault()
    local anchor = _G.EmoteMenuLauncherButton or _G.ChatFrameMenuButton or _G.ChatFrame1
    mainFrame:ClearAllPoints()
    if anchor == _G.ChatFrame1 then
        mainFrame:SetPoint("BOTTOMLEFT", anchor, "TOPLEFT", 10, 12)
    else
        mainFrame:SetPoint("BOTTOMLEFT", anchor, "TOPRIGHT", 6, 6)
    end
end

EmoteMenu.Toggle = function()
    if not mainFrame then CreateUI() end
    CreateLauncherButton()
    if not mainFrame._moved then PositionMenuDefault() end
    if mainFrame:IsShown() then mainFrame:Hide() else mainFrame:Show() end
end

-- Slash and init
SLASH_EMOTEMENU1 = "/emotemenu"
SlashCmdList["EMOTEMENU"] = function() EmoteMenu.Toggle() end

local EmoteMenuInit = CreateFrame("Frame")
EmoteMenuInit:RegisterEvent("PLAYER_LOGIN")
EmoteMenuInit:RegisterEvent("PLAYER_ENTERING_WORLD")
EmoteMenuInit:SetScript("OnEvent", function(_, evt)
    if evt == "PLAYER_LOGIN" then
        if C_Timer and C_Timer.After then C_Timer.After(0.5, CreateLauncherButton) else CreateLauncherButton() end
        print("EmoteMenu loaded. Type /emotemenu to open.")
    elseif evt == "PLAYER_ENTERING_WORLD" then
        if C_Timer and C_Timer.After then C_Timer.After(2, function() CreateLauncherButton() end) else CreateLauncherButton() end
    end
end)
