local addonName, addonTable = ...
---@type MK
local MK = addonTable.MK
local core = addonTable
local TRANSLATE = core.TRANSLATE or function(msg) return msg end

core.FindGroupTab = core.FindGroupTab or {}
local FindGroupTab = core.FindGroupTab
core.FindGroupList = core.FindGroupList or {}
local FindGroupList = core.FindGroupList
local FindGroupSearch = core.FindGroupSearch or {}
local MeetingStoneConstants = core.MeetingStoneConstants or {}
local MeetingStoneForm = core.MeetingStoneForm or {}

local TrySetAtlas = MeetingStoneForm.TrySetAtlas or function() return false end
local CreateSystemButton = MeetingStoneForm.CreateSystemButton

local ROLE_ATLAS_BY_KEY = core.GROUP_LIST_ROLE_ATLAS_BY_KEY or {
    TANK = "UI-LFG-RoleIcon-Tank",
    HEAL = "UI-LFG-RoleIcon-Healer",
    DPS = "UI-LFG-RoleIcon-DPS",
}

local ROLE_TEXT_KEY_BY_ROLE = {
    TANK = "坦克",
    HEAL = "治疗",
    DPS = "输出",
    UNKNOWN = "未知",
}

local MK_ICON_PATH = core.MK_ICON_PATH or ("Interface\\AddOns\\" .. addonName .. "\\Art\\Ui\\Icon\\")
local REFRESH_BUTTON_TEXTURE = "Interface\\AddOns\\" .. addonName .. "\\Art\\Ui\\Refresh.png"
local ROW_TEXTURE_NORMAL = "Interface\\AddOns\\" .. addonName .. "\\Art\\Ui\\ApplicantRowNormal.png"
local ROW_TEXTURE_GREEN = "Interface\\AddOns\\" .. addonName .. "\\Art\\Ui\\ApplicantRowGreen.png"
local ROW_TEXTURE_RED = "Interface\\AddOns\\" .. addonName .. "\\Art\\Ui\\ApplicantRowRed.png"
local ROW_TEXTURE_BLUE = "Interface\\AddOns\\" .. addonName .. "\\Art\\Ui\\ApplicantRowBlue.png"
local ROW_TEXTURE_GREY = "Interface\\AddOns\\" .. addonName .. "\\Art\\Ui\\ApplicantRowGrey.png"
local LEAVER_ICON_TEXTURE = MK_ICON_PATH .. "isLeaver.png"
local NEWBIE_ICON_TEXTURE = MK_ICON_PATH .. "Newbie.png"
local LEADER_ICON_TEXTURE = MK_ICON_PATH .. "Leader.png"
local STAR_ICON_TEXTURE = MK_ICON_PATH .. "Star.png"
local BNET_ICON_TEXTURE = MK_ICON_PATH .. "BattleNet.png"
local APPLICATION_ALERT_ICON_TEXTURE = "Interface\\DialogFrame\\UI-Dialog-Icon-AlertNew.blp"
local BLACKLIST_MENU_ICON_TEXTURE = MK_ICON_PATH .. "Blacklist.png"
local BLACKLIST_MENU_ICON_MARKUP = "|T" .. BLACKLIST_MENU_ICON_TEXTURE .. ":16:16:0:0|t "

local RIGHT_PANEL_OVERLAP = MeetingStoneConstants.MEETINGSTONE_RIGHT_PANEL_OVERLAP or -4

local HEADER_TOP_OFFSET = MeetingStoneConstants.MEETINGSTONE_HEADER_TOP_OFFSET or -20
local HEADER_BOTTOM_OFFSET = MeetingStoneConstants.MEETINGSTONE_HEADER_BOTTOM_OFFSET or -46
local HEADER_CONTENT_INSET_X = MeetingStoneConstants.MEETINGSTONE_HEADER_CONTENT_INSET_X or 4
local HEADER_CONTENT_OFFSET_Y = MeetingStoneConstants.MEETINGSTONE_HEADER_CONTENT_OFFSET_Y or 4

local TOP_RIGHT_FILIGREE_OFFSET_X = MeetingStoneConstants.MEETINGSTONE_TOP_RIGHT_FILIGREE_OFFSET_X or 0
local TOP_RIGHT_FILIGREE_OFFSET_Y = MeetingStoneConstants.MEETINGSTONE_TOP_RIGHT_FILIGREE_OFFSET_Y or -3
local BOTTOM_RIGHT_FILIGREE_OFFSET_X = MeetingStoneConstants.MEETINGSTONE_BOTTOM_RIGHT_FILIGREE_OFFSET_X or 0
local BOTTOM_RIGHT_FILIGREE_OFFSET_Y = MeetingStoneConstants.MEETINGSTONE_BOTTOM_RIGHT_FILIGREE_OFFSET_Y or 0
local APPLICANT_SCROLLBAR_WIDTH = MeetingStoneConstants.MEETINGSTONE_APPLICANT_SCROLLBAR_WIDTH or 17
local APPLICANT_SCROLLBAR_GAP = MeetingStoneConstants.MEETINGSTONE_APPLICANT_SCROLLBAR_GAP or 2
local APPLICANT_SCROLLBAR_RIGHT_INSET = MeetingStoneConstants.MEETINGSTONE_APPLICANT_SCROLLBAR_RIGHT_INSET or 10
local APPLICANT_SCROLL_BOTTOM_INSET = MeetingStoneConstants.MEETINGSTONE_APPLICANT_SCROLL_BOTTOM_INSET or 2

local HEADER_HEIGHT = math.abs(HEADER_BOTTOM_OFFSET - HEADER_TOP_OFFSET)
local ROW_HEIGHT = 36
local FIND_GROUP_VISIBLE_DETAIL_ROWS = 10

local ROW_SIDE_INSET = 4
local ROW_GAP = 1
local ROLE_ICON_SIZE = 16
local TYPE_ICON_SIZE = 16
local TYPE_ICON_LEFT_OFFSET = 10
local TYPE_TEXT_MIN_WIDTH = 54
local TYPE_COLUMN_MIN_WIDTH = TYPE_ICON_LEFT_OFFSET + TYPE_ICON_SIZE + 4 + TYPE_TEXT_MIN_WIDTH + 12
local TITLE_COLUMN_MIN_WIDTH = 86
local HEADER_TEXT_SIZE = MeetingStoneConstants.MEETINGSTONE_HEADER_TEXT_SIZE or 14
local HEADER_TEXT_INSET_X = MeetingStoneConstants.MEETINGSTONE_HEADER_TEXT_INSET_X or 3
local HEADER_TEXT_PRESSED_OFFSET_X = MeetingStoneConstants.MEETINGSTONE_HEADER_TEXT_PRESSED_OFFSET_X or 1
local HEADER_TEXT_PRESSED_OFFSET_Y = MeetingStoneConstants.MEETINGSTONE_HEADER_TEXT_PRESSED_OFFSET_Y or -1
local HEADER_TEXT_PRESSED_ALPHA = MeetingStoneConstants.MEETINGSTONE_HEADER_TEXT_PRESSED_ALPHA or 1
local HEADER_NORMAL_TEXT_COLOR = MeetingStoneConstants.MEETINGSTONE_HEADER_TEXT_COLOR or { 1, 0.82, 0, 1 }
local HEADER_HOVER_TEXT_COLOR = MeetingStoneConstants.MEETINGSTONE_HEADER_HOVER_TEXT_COLOR or { 1, 0.96, 0.58, 1 }
local HEADER_PRESSED_TEXT_COLOR = MeetingStoneConstants.MEETINGSTONE_HEADER_PRESSED_TEXT_COLOR or { 0.95, 0.68, 0.18, 1 }
local HEADER_ACCENT_WIDTH = MeetingStoneConstants.MEETINGSTONE_HEADER_ACCENT_WIDTH or 1.5
local HEADER_ACCENT_HEIGHT = MeetingStoneConstants.MEETINGSTONE_HEADER_ACCENT_HEIGHT or 22
local HEADER_ACCENT_COLOR = MeetingStoneConstants.MEETINGSTONE_HEADER_ACCENT_COLOR or { 126 / 255, 112 / 255, 82 / 255 }
local HEADER_ACCENT_ALPHA = MeetingStoneConstants.MEETINGSTONE_HEADER_ACCENT_ALPHA or 0.6
local GetHeaderAccentPixelWidth = MeetingStoneConstants.GetMeetingStoneHeaderAccentPixelWidth or function()
    return HEADER_ACCENT_WIDTH
end
local SnapHeaderAccentOffset = MeetingStoneConstants.SnapMeetingStoneHeaderAccentOffset or function(_, value)
    return math.floor((tonumber(value) or 0) + 0.5)
end
local COMMENT_TEXT_COLOR = { 1, 1, 1, 1 }
local APPLICATION_PENDING_TEXT_COLOR = { 0.12, 1, 0.25, 1 }
local APPLICATION_DECLINED_TEXT_COLOR = { 1, 0.18, 0.12, 1 }
local APPLICATION_CANCELLED_TEXT_COLOR = { 0.55, 0.55, 0.55, 1 }
local APPLICATION_MENU_APPLY_COLOR_CODE = "|cff1eff3f"
local BLACKLIST_MENU_COLOR_CODE = "|cffff2020"
local REQUIREMENT_TEXT_COLOR = { 0.12, 1, 0.25, 1 }
local REQUIREMENT_EMPTY_TEXT_COLOR = { 0.5, 0.5, 0.5, 1 }
local EMPTY_TEXT_COLOR = core.PANEL_EMPTY_PROMPT_TEXT_COLOR or { 1, 0.82, 0, 1 }
local EMPTY_TEAMUP_TEXTURE = "Interface\\AddOns\\" .. addonName .. "\\Art\\Ui\\TeamUp.png"
local EMPTY_TEAMUP_ICON_COUNT = 3
local EMPTY_TEAMUP_ICON_WIDTH = 27
local EMPTY_TEAMUP_ICON_HEIGHT = 25
local EMPTY_TEAMUP_ICON_GAP = 6
local EMPTY_TEAMUP_TEXT_GAP = 7
local EMPTY_TEAMUP_STEP_SECONDS = 0.28
local EMPTY_TEAMUP_FADE_IN_SECONDS = 0.16
local EMPTY_TEAMUP_HOLD_SECONDS = 0.4
local EMPTY_TEAMUP_FADE_OUT_SECONDS = 0.45
local ROW_FADE_IN_SECONDS = 0.18
local ROW_FADE_OUT_SECONDS = 0.18
local ROW_BACKGROUND_FADE_OUT_SECONDS = 0.45
local ROW_HOVER_HEIGHT = core.GROUP_LIST_ROW_HOVER_HEIGHT or 28
local ROW_HOVER_INSET_X = core.GROUP_LIST_ROW_HOVER_INSET_X or 2
local ROW_HOVER_OFFSET_Y = core.GROUP_LIST_ROW_HOVER_OFFSET_Y or 0
local ROW_HOVER_FADE_WIDTH = core.GROUP_LIST_ROW_HOVER_FADE_WIDTH or 96
local APPLICATION_CANCEL_BUTTON_SIZE = 32
local APPLICATION_CANCEL_BUTTON_SCALE = 0.7
local APPLICATION_CANCEL_BUTTON_DISPLAY_SIZE = APPLICATION_CANCEL_BUTTON_SIZE * APPLICATION_CANCEL_BUTTON_SCALE
local APPLICATION_CANCEL_BUTTON_ICON_SIZE = 16
local APPLICATION_CANCEL_BUTTON_GAP = 4
local APPLICATION_CANCEL_BUTTON_ATLAS = "UI-LFG-DeclineMark"
local APPLICATION_STATUS_ICON_SIZE = 16
local APPLICATION_STATUS_ICON_GAP = 3
local APPLICATION_PENDING_SPINNER_SIZE = 18
local HEADER_REFRESH_BUTTON_SIZE = 35
local HEADER_REFRESH_ICON_SIZE = 21
local HEADER_REFRESH_ICON_PRESSED_SIZE = 19
local HEADER_REFRESH_BUTTON_OFFSET_Y = 6

local COLUMN_WIDTHS = {
    title = 108,
    dungeon = 108,
    type = 96,
    members = 96,
    requirement = 56,
    leader = 92,
    score = 54,
    comment = 130,
}

local COLUMN_ORDER = {
    "title",
    "dungeon",
    "type",
    "members",
    "requirement",
    "leader",
    "score",
    "comment",
}

local HEADER_LABELS = {
    title = "队伍标题",
    dungeon = "副本名称",
    type = "类型",
    members = "队伍成员列",
    requirement = "要求",
    leader = "队长",
    score = "分数",
    comment = "说明",
}

local SORTABLE_HEADER_KEYS = {
    dungeon = true,
    members = true,
    requirement = true,
    score = true,
}

local function ColorText(text, color)
    if color and color.GenerateHexColorMarkup then
        return color:GenerateHexColorMarkup() .. tostring(text or "") .. "|r"
    end
    if type(color) == "table" and color.colorStr then
        return "|c" .. color.colorStr .. tostring(text or "") .. "|r"
    end
    return tostring(text or "")
end

local GetClassColor

local function IsUnknownMemberName(name)
    name = strtrim and strtrim(tostring(name or "")) or tostring(name or "")
    if name == "" or name == "-" or name == "未知目标" or name == "Unknown" or name == "Unknown Target" then
        return true
    end
    if _G.UNKNOWNOBJECT and name == _G.UNKNOWNOBJECT then
        return true
    end
    if _G.UNKNOWN and name == _G.UNKNOWN then
        return true
    end
    if _G.UNKNOWNBEING and name == _G.UNKNOWNBEING then
        return true
    end
    return false
end

local function GetTooltipMemberName(member)
    local memberName = member and (member.displayName or member.name)
    if IsUnknownMemberName(memberName) then
        return nil
    end
    return ColorText(memberName, GetClassColor(member.classFile))
end

local function GetInlineTexture(texture, size)
    size = size or 16
    return string.format("|T%s:%d:%d:0:0|t", tostring(texture or ""), size, size)
end

local function AddTypeMemberTooltipLine(tooltip, texture, label, names, r, g, b)
    if not (tooltip and type(names) == "table" and #names > 0) then
        return false
    end
    local text = string.format(
        TRANSLATE("%s：%s"),
        label,
        table.concat(names, TRANSLATE("、"))
    )
    tooltip:AddLine(GetInlineTexture(texture, 16) .. " " .. text, r, g, b, true)
    return true
end

GetClassColor = function(classFile)
    local colors = CUSTOM_CLASS_COLORS or RAID_CLASS_COLORS
    return classFile and colors and colors[classFile] or nil
end

local function GetScoreColor(score)
    score = tonumber(score) or 0
    if C_ChallengeMode and C_ChallengeMode.GetDungeonScoreRarityColor then
        local color = C_ChallengeMode.GetDungeonScoreRarityColor(score)
        if color then
            return color
        end
    end
    return HIGHLIGHT_FONT_COLOR
end

local function GetColumnLayout(availableWidth)
    local layout = {}
    local widths = {}
    local baseTotal = ROW_SIDE_INSET * 2
    for _, key in ipairs(COLUMN_ORDER) do
        baseTotal = baseTotal + (COLUMN_WIDTHS[key] or 0)
    end
    baseTotal = baseTotal + math.max(0, #COLUMN_ORDER - 1) * ROW_GAP

    local scale = 1
    local extraCommentWidth = 0
    if availableWidth and availableWidth > 0 then
        if availableWidth < baseTotal then
            scale = math.max(0.76, availableWidth / baseTotal)
        elseif availableWidth > baseTotal then
            extraCommentWidth = availableWidth - baseTotal
        end
    end

    for _, key in ipairs(COLUMN_ORDER) do
        widths[key] = (COLUMN_WIDTHS[key] or 0) * scale
    end

    local typeDeficit = TYPE_COLUMN_MIN_WIDTH - (widths.type or 0)
    if typeDeficit > 0 and (widths.title or 0) > TITLE_COLUMN_MIN_WIDTH then
        local transfer = math.min(typeDeficit, (widths.title or 0) - TITLE_COLUMN_MIN_WIDTH)
        widths.title = widths.title - transfer
        widths.type = widths.type + transfer
    end

    local x = ROW_SIDE_INSET * scale
    for index, key in ipairs(COLUMN_ORDER) do
        local width = widths[key] or 0
        if key == "comment" then
            width = width + extraCommentWidth
        end
        layout[key] = { x = x, width = math.max(1, width) }
        x = x + width
        if index < #COLUMN_ORDER then
            x = x + ROW_GAP
        end
    end
    layout.totalWidth = x + ROW_SIDE_INSET * scale
    layout.scale = scale
    return layout
end

local function CreatePlainLabel(parent, text, size, justify)
    local label = parent:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    label:SetText(text or "")
    label:SetJustifyH(justify or "LEFT")
    label:SetJustifyV("MIDDLE")
    label:SetWordWrap(false)
    if core.ApplyFont then
        core.ApplyFont(label, size or 12, "")
    end
    return label
end

local function CreateFindGroupFrameWithTemplateOptions(frameType, name, parent, templates)
    for _, template in ipairs(templates or {}) do
        local ok, frame = pcall(CreateFrame, frameType, name, parent, template)
        if ok and frame then
            return frame
        end
    end
    return CreateFrame(frameType, name, parent)
end

local function CreateFindGroupScrollContainer(parent)
    local scrollFrame = CreateFrame("ScrollFrame", nil, parent)
    scrollFrame:EnableMouseWheel(true)

    local content = CreateFrame("Frame", nil, scrollFrame)
    content:SetPoint("TOPLEFT", scrollFrame, "TOPLEFT", 0, 0)
    content:SetWidth(1)
    content:SetHeight(1)
    scrollFrame:SetScrollChild(content)

    local scrollBar = CreateFindGroupFrameWithTemplateOptions("EventFrame", nil, parent, { "MinimalScrollBar" })
    if not scrollBar or (not scrollBar.SetValue and not scrollBar.SetScrollPercentage) then
        scrollBar = CreateFrame("Slider", nil, parent)
        scrollBar:SetOrientation("VERTICAL")
        scrollBar:SetValueStep(1)
    end
    scrollBar:SetWidth(APPLICANT_SCROLLBAR_WIDTH)
    if scrollBar.SetMinMaxValues then
        scrollBar:SetMinMaxValues(0, 0)
    end
    scrollBar:Hide()

    local useNativeMinimalScrollBar = scrollBar.Init and ScrollUtil and ScrollUtil.InitScrollFrameWithScrollBar
    if useNativeMinimalScrollBar then
        ScrollUtil.InitScrollFrameWithScrollBar(scrollFrame, scrollBar)
    end

    local isSyncingScroll = false
    local scrollMinValue = 0
    local scrollMaxValue = 0

    local function setScrollBarRange(minValue, maxValue)
        scrollMinValue = minValue or 0
        scrollMaxValue = maxValue or 0
        if scrollBar.SetMinMaxValues then
            scrollBar:SetMinMaxValues(scrollMinValue, scrollMaxValue)
        elseif scrollBar.SetScrollPercentage then
            scrollBar:SetScrollPercentage(0)
        end
    end
    scrollBar.mkSetScrollRange = setScrollBarRange

    local function getScrollBarValue()
        if useNativeMinimalScrollBar and scrollBar.GetScrollPercentage then
            return (scrollBar:GetScrollPercentage() or 0) * scrollMaxValue
        end
        if scrollBar.GetValue then
            return scrollBar:GetValue() or 0
        end
        if scrollBar.GetScrollPercentage then
            return (scrollBar:GetScrollPercentage() or 0) * scrollMaxValue
        end
        return 0
    end
    scrollBar.mkGetScrollValue = getScrollBarValue

    local function setScrollBarValue(value)
        if useNativeMinimalScrollBar and scrollBar.SetScrollPercentage then
            local percentage = scrollMaxValue > 0 and ((value or 0) / scrollMaxValue) or 0
            scrollBar:SetScrollPercentage(math.min(math.max(percentage, 0), 1), ScrollBoxConstants and ScrollBoxConstants.NoScrollInterpolation)
        elseif scrollBar.SetValue then
            scrollBar:SetValue(value or 0)
        elseif scrollBar.SetScrollPercentage then
            local percentage = scrollMaxValue > 0 and ((value or 0) / scrollMaxValue) or 0
            scrollBar:SetScrollPercentage(math.min(math.max(percentage, 0), 1))
        end
    end
    scrollBar.mkSetScrollValue = setScrollBarValue

    local function applyScrollValue(value)
        local nextValue = math.min(math.max(value or 0, scrollMinValue), scrollMaxValue)
        isSyncingScroll = true
        scrollFrame:SetVerticalScroll(nextValue)
        if math.abs(getScrollBarValue() - nextValue) > 0.5 then
            setScrollBarValue(nextValue)
        end
        isSyncingScroll = false
        if scrollFrame.mkOnScrollValueChanged then
            scrollFrame.mkOnScrollValueChanged(scrollFrame, nextValue)
        end
    end

    if useNativeMinimalScrollBar then
        -- ScrollUtil.InitScrollFrameWithScrollBar wires the arrows, thumb, mouse wheel, and ScrollFrame sync.
    elseif scrollBar.SetScrollPercentage and scrollBar.RegisterCallback and scrollBar.Event and scrollBar.Event.OnScroll then
        scrollBar:RegisterCallback(scrollBar.Event.OnScroll, function(_, scrollPercentage)
            if not isSyncingScroll then
                applyScrollValue((scrollPercentage or 0) * scrollMaxValue)
            end
        end)
    elseif scrollBar.SetScript and scrollBar.SetValue then
        scrollBar:SetScript("OnValueChanged", function(_, value)
            if not isSyncingScroll then
                applyScrollValue(value or getScrollBarValue())
            end
        end)
    end
    setScrollBarValue(0)

    if not useNativeMinimalScrollBar then
        scrollFrame:SetScript("OnVerticalScroll", function(_, offset)
            applyScrollValue(offset)
        end)
        scrollFrame:SetScript("OnMouseWheel", function(_, delta)
            applyScrollValue(getScrollBarValue() - (delta or 0) * ROW_HEIGHT)
        end)
    end
    scrollFrame:HookScript("OnVerticalScroll", function(frame, offset)
        if frame.mkOnScrollValueChanged then
            frame.mkOnScrollValueChanged(frame, offset or frame:GetVerticalScroll() or 0)
        end
    end)

    scrollFrame:SetScript("OnSizeChanged", function(frame)
        content:SetWidth(math.max(frame:GetWidth() or 0, 1))
    end)

    return scrollFrame, content, scrollBar
end

local function SetFindGroupScrollValue(scrollFrame, scrollBar, value)
    local nextValue = value or 0
    if scrollFrame and scrollFrame.SetVerticalScroll then
        scrollFrame:SetVerticalScroll(nextValue)
    end
    if scrollBar and scrollBar.mkSetScrollValue then
        scrollBar.mkSetScrollValue(nextValue)
    elseif scrollBar and scrollBar.SetValue then
        scrollBar:SetValue(nextValue)
    end
end

local function GetFindGroupLoadingCycleSeconds()
    return (EMPTY_TEAMUP_STEP_SECONDS * EMPTY_TEAMUP_ICON_COUNT)
        + EMPTY_TEAMUP_HOLD_SECONDS
        + EMPTY_TEAMUP_FADE_OUT_SECONDS
end

local function GetFindGroupLoadingIconAlpha(elapsed, iconIndex)
    local appearAt = (iconIndex - 1) * EMPTY_TEAMUP_STEP_SECONDS
    if elapsed < appearAt then
        return 0
    end

    local fadeInEnd = appearAt + EMPTY_TEAMUP_FADE_IN_SECONDS
    if elapsed < fadeInEnd then
        return math.max(0, math.min(1, (elapsed - appearAt) / EMPTY_TEAMUP_FADE_IN_SECONDS))
    end

    local fadeOutStart = (EMPTY_TEAMUP_STEP_SECONDS * EMPTY_TEAMUP_ICON_COUNT) + EMPTY_TEAMUP_HOLD_SECONDS
    if elapsed < fadeOutStart then
        return 1
    end

    local fadeOutEnd = fadeOutStart + EMPTY_TEAMUP_FADE_OUT_SECONDS
    if elapsed < fadeOutEnd then
        return math.max(0, math.min(1, 1 - ((elapsed - fadeOutStart) / EMPTY_TEAMUP_FADE_OUT_SECONDS)))
    end

    return 0
end

local function RefreshFindGroupLoadingAnimation(animationFrame)
    if not (animationFrame and animationFrame.icons) then
        return
    end

    local elapsed = animationFrame.elapsed or 0
    for index, icon in ipairs(animationFrame.icons) do
        local alpha = GetFindGroupLoadingIconAlpha(elapsed, index)
        icon:SetAlpha(alpha)
        if alpha > 0.02 then
            icon:Show()
        else
            icon:Hide()
        end
    end
end

local function EnsureFindGroupLoadingAnimation(widgets)
    if not widgets then
        return nil
    end
    if widgets.findGroupLoadingAnimation then
        return widgets.findGroupLoadingAnimation
    end

    local parent = widgets.findGroupScrollFrame or widgets.findGroupRowsFrame
    if not parent then
        return nil
    end

    local frame = CreateFrame("Frame", nil, parent)
    frame:SetFrameLevel((parent.GetFrameLevel and parent:GetFrameLevel() or 0) + 5)
    local width = (EMPTY_TEAMUP_ICON_WIDTH * EMPTY_TEAMUP_ICON_COUNT)
        + (EMPTY_TEAMUP_ICON_GAP * (EMPTY_TEAMUP_ICON_COUNT - 1))
    frame:SetSize(width, EMPTY_TEAMUP_ICON_HEIGHT)
    frame.icons = {}

    for index = 1, EMPTY_TEAMUP_ICON_COUNT do
        local icon = frame:CreateTexture(nil, "ARTWORK")
        icon:SetTexture(EMPTY_TEAMUP_TEXTURE)
        icon:SetTexCoord((index - 1) / EMPTY_TEAMUP_ICON_COUNT, index / EMPTY_TEAMUP_ICON_COUNT, 0, 1)
        icon:SetSize(EMPTY_TEAMUP_ICON_WIDTH, EMPTY_TEAMUP_ICON_HEIGHT)
        icon:SetPoint("LEFT", frame, "LEFT", (index - 1) * (EMPTY_TEAMUP_ICON_WIDTH + EMPTY_TEAMUP_ICON_GAP), 0)
        icon:SetAlpha(0)
        icon:Hide()
        frame.icons[index] = icon
    end

    frame:SetScript("OnShow", function(animation)
        animation.elapsed = 0
        RefreshFindGroupLoadingAnimation(animation)
    end)
    frame:SetScript("OnUpdate", function(animation, elapsed)
        animation.elapsed = ((animation.elapsed or 0) + (elapsed or 0)) % GetFindGroupLoadingCycleSeconds()
        RefreshFindGroupLoadingAnimation(animation)
    end)
    frame:Hide()

    widgets.findGroupLoadingAnimation = frame
    return frame
end

local function SetFindGroupLoadingAnimationShown(widgets, shown, attachToEmpty)
    local animation = shown and EnsureFindGroupLoadingAnimation(widgets) or widgets and widgets.findGroupLoadingAnimation
    if not animation then
        return
    end

    animation:ClearAllPoints()
    if attachToEmpty and widgets and widgets.findGroupEmpty then
        animation:SetPoint("TOP", widgets.findGroupEmpty, "BOTTOM", 0, -EMPTY_TEAMUP_TEXT_GAP)
    elseif widgets and widgets.findGroupScrollFrame then
        animation:SetPoint("BOTTOM", widgets.findGroupScrollFrame, "BOTTOM", 0, 8)
    elseif widgets and widgets.findGroupRowsFrame then
        animation:SetPoint("BOTTOM", widgets.findGroupRowsFrame, "BOTTOM", 0, 8)
    end

    if shown then
        local wasShown = animation.IsShown and animation:IsShown()
        if not wasShown then
            animation.elapsed = 0
            RefreshFindGroupLoadingAnimation(animation)
        end
        animation:Show()
    else
        animation:Hide()
    end
end

local function UpdateFindGroupEmptyLayout(widgets, showAnimation)
    local empty = widgets and widgets.findGroupEmpty
    local rowsFrame = widgets and widgets.findGroupRowsFrame
    if not (empty and rowsFrame) then
        return
    end

    local offsetY = showAnimation and ((EMPTY_TEAMUP_ICON_HEIGHT + EMPTY_TEAMUP_TEXT_GAP) / 2) or 0
    empty:ClearAllPoints()
    empty:SetPoint("CENTER", rowsFrame, "CENTER", 0, offsetY)
end

local function SetHeaderTextPressed(label, pressed)
    local text = label and label.text
    if not text then
        return
    end
    label.mkHeaderTextPressed = pressed == true
    local offsetX = pressed and HEADER_TEXT_PRESSED_OFFSET_X or 0
    local offsetY = pressed and HEADER_TEXT_PRESSED_OFFSET_Y or 0
    local color = pressed and HEADER_PRESSED_TEXT_COLOR
        or (label.mkHeaderTextHovered and HEADER_HOVER_TEXT_COLOR or HEADER_NORMAL_TEXT_COLOR)
    text:ClearAllPoints()
    text:SetPoint("TOPLEFT", label, "TOPLEFT", HEADER_TEXT_INSET_X + offsetX, offsetY)
    text:SetPoint("BOTTOMRIGHT", label, "BOTTOMRIGHT", -HEADER_TEXT_INSET_X + offsetX, offsetY)
    text:SetAlpha(pressed and HEADER_TEXT_PRESSED_ALPHA or 1)
    text:SetTextColor(color[1], color[2], color[3], color[4])
end

local function SetHeaderTextHovered(label, hovered)
    if not label then
        return
    end
    label.mkHeaderTextHovered = hovered == true
    SetHeaderTextPressed(label, label.mkHeaderTextPressed == true)
end

local function RefreshHeaderSortState(header, owner)
    if not header then
        return
    end
    local sortKey = ""
    if FindGroupSearch.GetSort then
        sortKey = FindGroupSearch.GetSort(owner)
    end
    for _, key in ipairs(COLUMN_ORDER) do
        local label = header[key]
        if label and label.text then
            local text = TRANSLATE(HEADER_LABELS[key])
            label.text:SetText(text)
            label.text:SetTextColor(HEADER_NORMAL_TEXT_COLOR[1], HEADER_NORMAL_TEXT_COLOR[2], HEADER_NORMAL_TEXT_COLOR[3], HEADER_NORMAL_TEXT_COLOR[4])
        end
    end
end

local function CreateHeader(parent, owner)
    local frame = CreateFrame("Frame", nil, parent)
    frame:SetHeight(HEADER_HEIGHT)
    for _, key in ipairs(COLUMN_ORDER) do
        local columnKey = key
        local label = CreateFrame(SORTABLE_HEADER_KEYS[columnKey] and "Button" or "Frame", nil, frame)
        local text = CreatePlainLabel(label, TRANSLATE(HEADER_LABELS[key]), HEADER_TEXT_SIZE, "CENTER")
        label.text = text
        SetHeaderTextPressed(label, false)
        if SORTABLE_HEADER_KEYS[columnKey] then
            label:RegisterForClicks("LeftButtonUp")
            label:SetScript("OnEnter", function(self)
                SetHeaderTextHovered(self, true)
            end)
            label:SetScript("OnMouseDown", function(self)
                SetHeaderTextPressed(self, true)
            end)
            label:SetScript("OnMouseUp", function(self)
                SetHeaderTextPressed(self, false)
            end)
            label:SetScript("OnLeave", function(self)
                SetHeaderTextHovered(self, false)
                SetHeaderTextPressed(self, false)
            end)
            label:SetScript("OnClick", function()
                if FindGroupSearch.SetSort then
                    FindGroupSearch.SetSort(owner, columnKey)
                end
                RefreshHeaderSortState(frame, owner)
                if owner and owner.RefreshMainWindow then
                    owner:RefreshMainWindow("FindGroupSort", columnKey)
                end
            end)
        end
        frame[columnKey] = label
    end
    RefreshHeaderSortState(frame, owner)
    return frame
end

local function SetHeaderAccentGradient(texture, startAlpha, endAlpha)
    if not texture then
        return
    end

    if texture.SetGradient and CreateColor then
        local ok = pcall(
            texture.SetGradient,
            texture,
            "VERTICAL",
            CreateColor(HEADER_ACCENT_COLOR[1], HEADER_ACCENT_COLOR[2], HEADER_ACCENT_COLOR[3], startAlpha),
            CreateColor(HEADER_ACCENT_COLOR[1], HEADER_ACCENT_COLOR[2], HEADER_ACCENT_COLOR[3], endAlpha)
        )
        if ok then
            return
        end
    end

    if texture.SetGradientAlpha then
        texture:SetGradientAlpha(
            "VERTICAL",
            HEADER_ACCENT_COLOR[1], HEADER_ACCENT_COLOR[2], HEADER_ACCENT_COLOR[3], startAlpha,
            HEADER_ACCENT_COLOR[1], HEADER_ACCENT_COLOR[2], HEADER_ACCENT_COLOR[3], endAlpha
        )
    else
        texture:SetVertexColor(
            HEADER_ACCENT_COLOR[1],
            HEADER_ACCENT_COLOR[2],
            HEADER_ACCENT_COLOR[3],
            math.max(startAlpha or 0, endAlpha or 0)
        )
    end
end

local function LayoutHeaderAccent(accent)
    if not accent then
        return
    end

    local height = HEADER_ACCENT_HEIGHT
    local halfHeight = height / 2
    accent:SetSize(GetHeaderAccentPixelWidth(accent), height)

    if accent.top then
        accent.top:ClearAllPoints()
        accent.top:SetPoint("TOPLEFT", accent, "TOPLEFT", 0, 0)
        accent.top:SetPoint("TOPRIGHT", accent, "TOPRIGHT", 0, 0)
        accent.top:SetHeight(halfHeight)
    end
    if accent.bottom then
        accent.bottom:ClearAllPoints()
        accent.bottom:SetPoint("TOPLEFT", accent.top, "BOTTOMLEFT", 0, 0)
        accent.bottom:SetPoint("BOTTOMRIGHT", accent, "BOTTOMRIGHT", 0, 0)
    end
end

local function CreateHeaderAccent(parent)
    local accent = CreateFrame("Frame", nil, parent)

    local top = accent:CreateTexture(nil, "OVERLAY")
    top:SetTexture("Interface\\Buttons\\WHITE8X8")
    SetHeaderAccentGradient(top, HEADER_ACCENT_ALPHA, 0)

    local bottom = accent:CreateTexture(nil, "OVERLAY")
    bottom:SetTexture("Interface\\Buttons\\WHITE8X8")
    SetHeaderAccentGradient(bottom, 0, HEADER_ACCENT_ALPHA)

    accent.top = top
    accent.bottom = bottom
    LayoutHeaderAccent(accent)

    return accent
end

local function EnsureHeaderGapAccents(header)
    header.gapAccents = header.gapAccents or {}
    local requiredCount = math.max(0, #COLUMN_ORDER - 1)
    for index = #header.gapAccents + 1, requiredCount do
        header.gapAccents[index] = CreateHeaderAccent(header)
    end
    return header.gapAccents
end

local function ApplyHeaderLayout(header, availableWidth)
    local layout = GetColumnLayout(availableWidth)
    local gapAccents = header and EnsureHeaderGapAccents(header) or {}
    for _, key in ipairs(COLUMN_ORDER) do
        local label = header and header[key]
        if label and layout[key] then
            label:ClearAllPoints()
            label:SetPoint("LEFT", header, "LEFT", layout[key].x, 0)
            label:SetSize(layout[key].width, HEADER_HEIGHT)
            if label.text and core.ApplyFont then
                core.ApplyFont(label.text, layout.scale < 0.86 and 11 or 13, "")
            end
        end
    end
    for index = 1, #gapAccents do
        local leftKey = COLUMN_ORDER[index]
        local leftColumn = layout[leftKey]
        local accent = gapAccents[index]
        if leftColumn and accent then
            accent:ClearAllPoints()
            LayoutHeaderAccent(accent)
            accent:SetPoint("CENTER", header, "LEFT", SnapHeaderAccentOffset(header, leftColumn.x + leftColumn.width), 0)
            accent:Show()
        elseif accent then
            accent:Hide()
        end
    end
    header.mkFindGroupColumnLayout = layout
end

local function SetTextureAtlas(texture, atlasName)
    if not texture or not atlasName then
        return false
    end
    if texture.SetAtlas then
        local ok = pcall(texture.SetAtlas, texture, atlasName)
        if ok then
            return true
        end
    end
    return false
end

local function CreateFallbackFindGroupPendingSpinner(parent)
    if not (CreateFrame and parent) then
        return nil
    end

    local spinner = CreateFrame("Frame", nil, parent)
    spinner.Ring = spinner:CreateTexture(nil, "ARTWORK")
    spinner.Ring:SetAllPoints(spinner)
    spinner.Ring:SetAtlas("Spinner_Ring")
    spinner.Sparks = spinner:CreateTexture(nil, "ARTWORK")
    spinner.Sparks:SetAllPoints(spinner)
    spinner.Sparks:SetAtlas("Spinner_Sparks")
    if spinner.Sparks.SetBlendMode then
        spinner.Sparks:SetBlendMode("ADD")
    end
    return spinner
end

local function CreateFindGroupPendingSpinner(parent)
    if not (CreateFrame and parent) then
        return nil
    end

    local ok, spinner = pcall(CreateFrame, "Frame", nil, parent, "SpinnerTemplate")
    spinner = ok and spinner or CreateFallbackFindGroupPendingSpinner(parent)
    if spinner then
        spinner:SetSize(APPLICATION_PENDING_SPINNER_SIZE, APPLICATION_PENDING_SPINNER_SIZE)
        spinner:Hide()
    end
    return spinner
end

local function StopFindGroupPendingSpinner(spinner)
    if not spinner then
        return
    end
    if spinner.Anim and spinner.Anim.Stop then
        spinner.Anim:Stop()
    end
    spinner:Hide()
end

local function StartFindGroupPendingSpinner(spinner)
    if not spinner then
        return
    end
    spinner:SetSize(APPLICATION_PENDING_SPINNER_SIZE, APPLICATION_PENDING_SPINNER_SIZE)
    spinner:Show()
    if spinner.Anim and spinner.Anim.Play and (not spinner.Anim.IsPlaying or not spinner.Anim:IsPlaying()) then
        spinner.Anim:Play()
    end
end

local function PlayAlphaAnimation(frame, key, fromAlpha, toAlpha, duration, onFinished, smoothing)
    if not frame then
        return
    end
    if not frame.CreateAnimationGroup then
        frame:SetAlpha(toAlpha or 1)
        if onFinished then
            onFinished()
        end
        return
    end

    frame.mkFindGroupAnimations = frame.mkFindGroupAnimations or {}
    local group = frame.mkFindGroupAnimations[key]
    if not group then
        group = frame:CreateAnimationGroup()
        local alpha = group:CreateAnimation("Alpha")
        alpha:SetOrder(1)
        group.mkAlpha = alpha
        frame.mkFindGroupAnimations[key] = group
    end
    group:Stop()
    group.mkAlpha:SetFromAlpha(fromAlpha or 0)
    group.mkAlpha:SetToAlpha(toAlpha or 1)
    group.mkAlpha:SetDuration(duration or ROW_FADE_IN_SECONDS)
    if smoothing and group.mkAlpha.SetSmoothing then
        group.mkAlpha:SetSmoothing(smoothing)
    end
    group:SetScript("OnFinished", onFinished)
    frame:SetAlpha(fromAlpha or 0)
    group:Play()
end

local function StopAlphaAnimation(frame, key, alpha)
    if not frame then
        return
    end
    local group = frame.mkFindGroupAnimations and frame.mkFindGroupAnimations[key]
    if group then
        group:Stop()
    end
    if frame.SetAlpha then
        frame:SetAlpha(alpha or 1)
    end
end

local function MarkRowFadeComplete(row, resultID, widgets, state)
    local key = tostring(resultID or "")
    if key == "" then
        return nil, nil
    end

    if widgets then
        local requestID = tonumber(state and state.searchRequestID) or 0
        if widgets.mkFindGroupFadeSearchRequestID ~= requestID then
            widgets.mkFindGroupFadeSearchRequestID = requestID
            widgets.mkFindGroupFadedResultIDs = {}
        end
        widgets.mkFindGroupFadedResultIDs = widgets.mkFindGroupFadedResultIDs or {}
        widgets.mkFindGroupFadedResultIDs[key] = true
        row.mkFindGroupFadeKey = key
        return key, widgets.mkFindGroupFadedResultIDs
    end
    row.mkFindGroupFadeKey = key
    return key, nil
end

local function CompleteRowFadeIn(row, resultID, widgets, state, fadeToken)
    if not row then
        return
    end
    if fadeToken and row.mkFindGroupFadeToken ~= fadeToken then
        return
    end
    if tostring(row.resultID or "") ~= tostring(resultID or "") then
        return
    end

    MarkRowFadeComplete(row, resultID, widgets, state)
    if row.SetAlpha then
        row:SetAlpha(1)
    end
end

local function PlayRowFadeIn(row, resultID, widgets, state, animate)
    local key = tostring(resultID or "")
    if key == "" then
        row.mkFindGroupFadeToken = (tonumber(row.mkFindGroupFadeToken) or 0) + 1
        StopAlphaAnimation(row, "appear", 1)
        return
    end

    if animate == false then
        row.mkFindGroupFadeToken = (tonumber(row.mkFindGroupFadeToken) or 0) + 1
        MarkRowFadeComplete(row, resultID, widgets, state)
        StopAlphaAnimation(row, "appear", 1)
        return
    end

    local fadedResults
    if widgets then
        local requestID = tonumber(state and state.searchRequestID) or 0
        if widgets.mkFindGroupFadeSearchRequestID ~= requestID then
            widgets.mkFindGroupFadeSearchRequestID = requestID
            widgets.mkFindGroupFadedResultIDs = {}
        end
        widgets.mkFindGroupFadedResultIDs = widgets.mkFindGroupFadedResultIDs or {}
        fadedResults = widgets.mkFindGroupFadedResultIDs
        if fadedResults[key] then
            row.mkFindGroupFadeToken = (tonumber(row.mkFindGroupFadeToken) or 0) + 1
            row.mkFindGroupFadeKey = key
            StopAlphaAnimation(row, "appear", 1)
            return
        end
    elseif row.mkFindGroupFadeKey == key then
        row.mkFindGroupFadeToken = (tonumber(row.mkFindGroupFadeToken) or 0) + 1
        StopAlphaAnimation(row, "appear", 1)
        return
    end

    row.mkFindGroupFadeKey = key
    row.mkFindGroupFadeToken = (tonumber(row.mkFindGroupFadeToken) or 0) + 1
    local fadeToken = row.mkFindGroupFadeToken
    PlayAlphaAnimation(row, "appear", 0, 1, ROW_FADE_IN_SECONDS, function()
        CompleteRowFadeIn(row, resultID, widgets, state, fadeToken)
    end)
    if C_Timer and C_Timer.After then
        C_Timer.After(ROW_FADE_IN_SECONDS + 0.05, function()
            CompleteRowFadeIn(row, resultID, widgets, state, fadeToken)
        end)
    end
end

local function PlayRowFadeOut(row, result)
    local resultID = result and result.resultID
    local token = result and result.reportRemovalToken
    if not (row and resultID and token) then
        return
    end
    if row.mkFindGroupRemovalResultID == resultID and row.mkFindGroupRemovalToken == token then
        return
    end

    row.mkFindGroupRemovalResultID = resultID
    row.mkFindGroupRemovalToken = token
    StopAlphaAnimation(row, "appear", row.GetAlpha and row:GetAlpha() or 1)
    local function finishRemoval()
        if row.mkFindGroupRemovalResultID ~= resultID or row.mkFindGroupRemovalToken ~= token then
            return
        end
        if FindGroupSearch.CompleteReportRemoval then
            FindGroupSearch.CompleteReportRemoval(MK, resultID, token)
        end
    end
    PlayAlphaAnimation(row, "remove", row.GetAlpha and row:GetAlpha() or 1, 0, ROW_FADE_OUT_SECONDS, finishRemoval, "IN")
    if C_Timer and C_Timer.After then
        C_Timer.After(ROW_FADE_OUT_SECONDS + 0.05, finishRemoval)
    else
        finishRemoval()
    end
end

local function SetRowBackground(row, texturePath, color, animate)
    local widgets = row and row.widgets
    local background = widgets and widgets.background
    if not background then
        return
    end

    color = color or { 1, 1, 1, 1 }
    local key = table.concat({
        tostring(texturePath or ""),
        string.format("%.2f", color[1] or 1),
        string.format("%.2f", color[2] or 1),
        string.format("%.2f", color[3] or 1),
        string.format("%.2f", color[4] or 1),
    }, ":")

    if animate and row.mkFindGroupBackgroundKey and row.mkFindGroupBackgroundKey ~= key and widgets.backgroundTransition then
        widgets.backgroundTransition:SetTexture(row.mkFindGroupBackgroundTexture or ROW_TEXTURE_NORMAL)
        widgets.backgroundTransition:SetVertexColor(
            row.mkFindGroupBackgroundColorR or 1,
            row.mkFindGroupBackgroundColorG or 1,
            row.mkFindGroupBackgroundColorB or 1,
            row.mkFindGroupBackgroundColorA or 1
        )
        widgets.backgroundTransition:SetAlpha(0.92)
        widgets.backgroundTransition:Show()
        PlayAlphaAnimation(widgets.backgroundTransition, "background", 0.92, 0, ROW_BACKGROUND_FADE_OUT_SECONDS, function()
            widgets.backgroundTransition:Hide()
        end, "IN")
        background:SetTexture(texturePath or ROW_TEXTURE_NORMAL)
        background:SetVertexColor(color[1] or 1, color[2] or 1, color[3] or 1, color[4] or 1)
        background:SetAlpha(0.92)
    else
        background:SetTexture(texturePath or ROW_TEXTURE_NORMAL)
        background:SetVertexColor(color[1] or 1, color[2] or 1, color[3] or 1, color[4] or 1)
        background:SetAlpha(0.92)
    end
    row.mkFindGroupBackgroundKey = key
    row.mkFindGroupBackgroundTexture = texturePath or ROW_TEXTURE_NORMAL
    row.mkFindGroupBackgroundColorR = color[1] or 1
    row.mkFindGroupBackgroundColorG = color[2] or 1
    row.mkFindGroupBackgroundColorB = color[3] or 1
    row.mkFindGroupBackgroundColorA = color[4] or 1
end

local function SetRowHoverColor(row, color)
    if not (row and row.widgets) then
        return
    end
    color = color or { 1, 0.74, 0.18, 0.13 }
    local r, g, b = color[1] or 1, color[2] or 0.74, color[3] or 0.18
    local alpha = color[4] or 0.13
    if row.widgets.hoverLeft then
        if row.widgets.hoverLeft.SetGradient and CreateColor then
            row.widgets.hoverLeft:SetGradient("HORIZONTAL", CreateColor(r, g, b, 0), CreateColor(r, g, b, alpha))
        else
            row.widgets.hoverLeft:SetVertexColor(r, g, b, alpha * 0.6)
        end
    end
    if row.widgets.hoverRight then
        if row.widgets.hoverRight.SetGradient and CreateColor then
            row.widgets.hoverRight:SetGradient("HORIZONTAL", CreateColor(r, g, b, alpha), CreateColor(r, g, b, 0))
        else
            row.widgets.hoverRight:SetVertexColor(r, g, b, alpha * 0.6)
        end
    end
    if row.widgets.hover then
        row.widgets.hover:SetVertexColor(r, g, b, alpha)
    end
end

local function SetFindGroupRowHover(row, shown)
    if row and row.mkFindGroupContextMenuLocked and shown ~= true then
        shown = true
    end
    if MK and MK.SetGroupRowHover then
        MK:SetGroupRowHover(row, shown)
        return
    end
    if not (row and row.widgets) then
        return
    end
    if row.widgets.hoverLeft then
        row.widgets.hoverLeft:SetShown(shown == true)
    end
    if row.widgets.hover then
        row.widgets.hover:SetShown(shown == true)
    end
    if row.widgets.hoverRight then
        row.widgets.hoverRight:SetShown(shown == true)
    end
end

local function FinishFindGroupRowContextMenu(row, token)
    if not row or row.mkFindGroupContextMenuToken ~= token then
        return
    end
    row.mkFindGroupContextMenuLocked = nil
    row.mkFindGroupContextMenuToken = nil
    SetFindGroupRowHover(row, row.IsMouseOver and row:IsMouseOver())
end

local function LockFindGroupRowContextMenu(row)
    if not row then
        return nil
    end
    local token = (tonumber(row.mkFindGroupContextMenuToken) or 0) + 1
    row.mkFindGroupContextMenuToken = token
    row.mkFindGroupContextMenuLocked = true
    SetFindGroupRowHover(row, true)
    return token
end

local function CreateFindGroupHoverTextures(row)
    local hoverLeft = row:CreateTexture(nil, "BORDER", nil, -1)
    hoverLeft:SetPoint("LEFT", row, "LEFT", ROW_HOVER_INSET_X, ROW_HOVER_OFFSET_Y)
    hoverLeft:SetSize(ROW_HOVER_FADE_WIDTH, ROW_HOVER_HEIGHT)
    hoverLeft:SetTexture("Interface\\Buttons\\WHITE8X8")
    if hoverLeft.SetGradient and CreateColor then
        hoverLeft:SetGradient("HORIZONTAL", CreateColor(1, 0.74, 0.18, 0), CreateColor(1, 0.74, 0.18, 0.13))
    else
        hoverLeft:SetVertexColor(1, 0.74, 0.18, 0.08)
    end
    hoverLeft:Hide()

    local hoverRight = row:CreateTexture(nil, "BORDER", nil, -1)
    hoverRight:SetPoint("RIGHT", row, "RIGHT", -ROW_HOVER_INSET_X, ROW_HOVER_OFFSET_Y)
    hoverRight:SetSize(ROW_HOVER_FADE_WIDTH, ROW_HOVER_HEIGHT)
    hoverRight:SetTexture("Interface\\Buttons\\WHITE8X8")
    if hoverRight.SetGradient and CreateColor then
        hoverRight:SetGradient("HORIZONTAL", CreateColor(1, 0.74, 0.18, 0.13), CreateColor(1, 0.74, 0.18, 0))
    else
        hoverRight:SetVertexColor(1, 0.74, 0.18, 0.08)
    end
    hoverRight:Hide()

    local hover = row:CreateTexture(nil, "BORDER", nil, -1)
    hover:SetPoint("LEFT", hoverLeft, "RIGHT", 0, 0)
    hover:SetPoint("RIGHT", hoverRight, "LEFT", 0, 0)
    hover:SetHeight(ROW_HOVER_HEIGHT)
    hover:SetTexture("Interface\\Buttons\\WHITE8X8")
    hover:SetVertexColor(1, 0.74, 0.18, 0.13)
    hover:Hide()

    return hoverLeft, hover, hoverRight
end

local function GetSpecIcon(entry)
    local specID = tonumber(entry and entry.specID)
    if specID and GetSpecializationInfoByID then
        local icon = select(4, GetSpecializationInfoByID(specID))
        if icon then
            return icon
        end
    end
    return nil
end

local function SetRoleIcon(texture, entry)
    local specIcon = GetSpecIcon(entry)
    if specIcon then
        texture:SetTexture(specIcon)
        texture:SetTexCoord(0, 1, 0, 1)
        return
    end
    texture:SetTexture(nil)
    if not SetTextureAtlas(texture, ROLE_ATLAS_BY_KEY[entry and entry.roleKey]) then
        texture:SetTexture("Interface\\LFGFrame\\UI-LFG-ICON-ROLES")
        texture:SetTexCoord(0, 1, 0, 1)
    end
end

local function AnchorFindGroupCancelButtonIcon(button, x, y)
    local icon = button and button.mkFindGroupCancelIcon
    if not icon then
        return
    end
    icon:ClearAllPoints()
    icon:SetPoint("CENTER", button, "CENTER", x or 0, y or 0)
end

local function CreateFindGroupApplicationCancelButton(parent)
    local button
    if CreateSystemButton then
        button = CreateSystemButton(parent, nil, APPLICATION_CANCEL_BUTTON_SIZE, APPLICATION_CANCEL_BUTTON_SIZE, "", 0)
    else
        button = CreateFrame("Button", nil, parent, "UIPanelButtonTemplate")
        button:SetSize(APPLICATION_CANCEL_BUTTON_SIZE, APPLICATION_CANCEL_BUTTON_SIZE)
    end
    button:SetSize(APPLICATION_CANCEL_BUTTON_SIZE, APPLICATION_CANCEL_BUTTON_SIZE)
    button:SetScale(APPLICATION_CANCEL_BUTTON_SCALE)
    button:SetFrameLevel(parent:GetFrameLevel() + 4)
    button:SetPoint("CENTER", parent, "CENTER", 0, 0)
    button:RegisterForClicks("LeftButtonUp")
    button:Hide()

    local label = button.Text or (button.GetFontString and button:GetFontString())
    if label then
        label:SetText("")
        label:Hide()
    end

    local icon = button:CreateTexture(nil, "OVERLAY")
    icon:SetSize(APPLICATION_CANCEL_BUTTON_ICON_SIZE, APPLICATION_CANCEL_BUTTON_ICON_SIZE)
    icon:SetVertexColor(1, 1, 1, 1)
    if not SetTextureAtlas(icon, APPLICATION_CANCEL_BUTTON_ATLAS) then
        icon:SetTexture("Interface\\Buttons\\UI-Panel-MinimizeButton-Up")
        icon:SetTexCoord(0, 1, 0, 1)
    end
    button.mkFindGroupCancelIcon = icon
    AnchorFindGroupCancelButtonIcon(button, 0, 0)

    button:SetScript("OnEnter", function(self)
        if core.MeetingStoneButtonArt and core.MeetingStoneButtonArt.Update then
            core.MeetingStoneButtonArt.Update(self)
        end
        if GameTooltip then
            GameTooltip:SetOwner(self, "ANCHOR_RIGHT")
            GameTooltip:SetText(TRANSLATE("取消申请"))
            GameTooltip:Show()
        end
    end)
    button:SetScript("OnLeave", function(self)
        self.mkFindGroupButtonPressed = nil
        if core.MeetingStoneButtonArt and core.MeetingStoneButtonArt.Update then
            core.MeetingStoneButtonArt.Update(self)
        end
        AnchorFindGroupCancelButtonIcon(self, 0, 0)
        GameTooltip_Hide()
    end)
    button:SetScript("OnMouseDown", function(self, mouseButton)
        if mouseButton == "LeftButton" then
            self.mkFindGroupButtonPressed = true
            if core.MeetingStoneButtonArt and core.MeetingStoneButtonArt.Update then
                core.MeetingStoneButtonArt.Update(self)
            end
            AnchorFindGroupCancelButtonIcon(self, 1, -1)
        end
    end)
    button:SetScript("OnMouseUp", function(self)
        self.mkFindGroupButtonPressed = nil
        if core.MeetingStoneButtonArt and core.MeetingStoneButtonArt.Update then
            core.MeetingStoneButtonArt.Update(self)
        end
        AnchorFindGroupCancelButtonIcon(self, 0, 0)
    end)
    button:SetScript("OnClick", function(self)
        local resultID = tonumber(self.resultID)
        if not resultID then
            return
        end
        local ok, reason = FindGroupSearch.CancelApplication(MK, resultID)
        if not ok and MK and MK.Print then
            MK:Print(reason)
        end
        if MK and MK.RefreshMainWindow then
            MK:RefreshMainWindow("FindGroupCancelApplication", tostring(resultID))
        end
    end)

    return button
end

local function CreateResultRow(parent)
    local row = CreateFrame("Button", nil, parent)
    row:SetHeight(ROW_HEIGHT)
    row:RegisterForClicks("LeftButtonUp", "RightButtonUp")
    row:EnableMouse(true)

    local background = row:CreateTexture(nil, "BACKGROUND")
    background:SetAllPoints(row)
    background:SetTexture(ROW_TEXTURE_NORMAL)
    background:SetAlpha(0.92)

    local backgroundTransition = row:CreateTexture(nil, "BACKGROUND", nil, 1)
    backgroundTransition:SetAllPoints(row)
    backgroundTransition:SetAlpha(0)
    backgroundTransition:Hide()

    local hoverLeft, hover, hoverRight = CreateFindGroupHoverTextures(row)

    local title = CreatePlainLabel(row, "", 12, "LEFT")
    local dungeon = CreatePlainLabel(row, "", 12, "LEFT")
    local typeContainer = CreateFrame("Frame", nil, row)
    local typeIcon = typeContainer:CreateTexture(nil, "OVERLAY")
    typeIcon:SetSize(TYPE_ICON_SIZE, TYPE_ICON_SIZE)
    local typeText = CreatePlainLabel(typeContainer, "", 12, "LEFT")
    local roles = {}
    local rolesContainer = CreateFrame("Frame", nil, row)
    for index = 1, 5 do
        local icon = rolesContainer:CreateTexture(nil, "OVERLAY")
        icon:SetSize(ROLE_ICON_SIZE, ROLE_ICON_SIZE)
        icon:Hide()
        roles[index] = icon
    end
    local requirement = CreatePlainLabel(row, "", 12, "CENTER")
    local leader = CreatePlainLabel(row, "", 12, "LEFT")
    local score = CreatePlainLabel(row, "", 12, "CENTER")
    local comment = CreatePlainLabel(row, "", 12, "LEFT")
    local applicationStatusIcon = row:CreateTexture(nil, "OVERLAY")
    applicationStatusIcon:SetSize(APPLICATION_STATUS_ICON_SIZE, APPLICATION_STATUS_ICON_SIZE)
    applicationStatusIcon:Hide()
    local applicationPendingSpinner = CreateFindGroupPendingSpinner(row)
    local cancelApplicationButtonHost = CreateFrame("Frame", nil, row)
    cancelApplicationButtonHost:SetSize(APPLICATION_CANCEL_BUTTON_DISPLAY_SIZE, APPLICATION_CANCEL_BUTTON_DISPLAY_SIZE)
    cancelApplicationButtonHost:SetFrameLevel(row:GetFrameLevel() + 4)
    cancelApplicationButtonHost:Hide()
    local cancelApplicationButton = CreateFindGroupApplicationCancelButton(cancelApplicationButtonHost)

    row.widgets = {
        background = background,
        backgroundTransition = backgroundTransition,
        hoverLeft = hoverLeft,
        hover = hover,
        hoverRight = hoverRight,
        title = title,
        dungeon = dungeon,
        typeContainer = typeContainer,
        typeIcon = typeIcon,
        typeText = typeText,
        rolesContainer = rolesContainer,
        roles = roles,
        requirement = requirement,
        leader = leader,
        score = score,
        comment = comment,
        applicationStatusIcon = applicationStatusIcon,
        applicationPendingSpinner = applicationPendingSpinner,
        cancelApplicationButtonHost = cancelApplicationButtonHost,
        cancelApplicationButton = cancelApplicationButton,
    }

    row:SetScript("OnEnter", function(self)
        SetFindGroupRowHover(self, true)
        FindGroupTab.ShowTooltip(self)
    end)
    row:SetScript("OnLeave", function(self)
        SetFindGroupRowHover(self, false)
        if GameTooltip and GameTooltip.GetOwner and GameTooltip:GetOwner() == self then
            GameTooltip:Hide()
        end
    end)
    row:SetScript("OnMouseUp", function(self, button)
        if button == "RightButton" then
            FindGroupTab.ShowContextMenu(self)
        elseif button == "LeftButton" then
            FindGroupTab.HandleRowClick(self)
        end
    end)

    return row
end

local function ApplyRowLayout(row, availableWidth)
    local layout = GetColumnLayout(availableWidth)
    local widgets = row and row.widgets
    if not widgets then
        return
    end
    row.mkFindGroupColumnLayout = layout
    local function setText(key, object)
        local column = layout[key]
        object:ClearAllPoints()
        object:SetPoint("LEFT", row, "LEFT", column.x + 2, 0)
        object:SetSize(math.max(1, column.width - 4), 18)
    end
    local function setFrame(key, object, height)
        local column = layout[key]
        object:ClearAllPoints()
        object:SetPoint("LEFT", row, "LEFT", column.x, 0)
        object:SetSize(column.width, height or 18)
    end

    setText("title", widgets.title)
    setText("dungeon", widgets.dungeon)
    setFrame("type", widgets.typeContainer, 18)
    setFrame("members", widgets.rolesContainer, ROLE_ICON_SIZE)
    setText("requirement", widgets.requirement)
    setText("leader", widgets.leader)
    setText("score", widgets.score)
    setText("comment", widgets.comment)
end

local function LayoutApplicationComment(row, applicationState)
    local widgets = row and row.widgets
    local layout = row and row.mkFindGroupColumnLayout
    local column = layout and layout.comment
    if not (widgets and widgets.comment and column) then
        return
    end

    local showCancelButton = applicationState == "pending"
    local button = widgets.cancelApplicationButton
    local buttonHost = widgets.cancelApplicationButtonHost or button
    local buttonWidth = APPLICATION_CANCEL_BUTTON_DISPLAY_SIZE
    local reservedWidth = showCancelButton and (buttonWidth + APPLICATION_CANCEL_BUTTON_GAP) or 0
    local availableWidth = math.max(1, column.width - 4 - reservedWidth)

    widgets.comment:ClearAllPoints()
    widgets.comment:SetJustifyV("MIDDLE")

    if buttonHost then
        buttonHost:ClearAllPoints()
        buttonHost:SetPoint("RIGHT", row, "LEFT", column.x + column.width - 2, 0)
        buttonHost:SetShown(showCancelButton)
    end
    if button then
        button:SetShown(showCancelButton)
    end

    local statusIcon = widgets.applicationStatusIcon
    local pendingSpinner = widgets.applicationPendingSpinner
    if not applicationState then
        if statusIcon then
            statusIcon:Hide()
        end
        StopFindGroupPendingSpinner(pendingSpinner)
        widgets.comment:SetPoint("LEFT", row, "LEFT", column.x + 2, 0)
        widgets.comment:SetSize(availableWidth, 18)
        widgets.comment:SetJustifyH("CENTER")
        return
    end

    local statusFrame
    if statusIcon then
        if applicationState == "pending" then
            statusIcon:Hide()
        else
            statusIcon:SetTexture(APPLICATION_ALERT_ICON_TEXTURE)
            statusIcon:SetTexCoord(0, 1, 0, 1)
            statusIcon:SetSize(APPLICATION_STATUS_ICON_SIZE, APPLICATION_STATUS_ICON_SIZE)
            statusFrame = statusIcon
        end
    end
    if applicationState == "pending" then
        statusFrame = pendingSpinner
    else
        StopFindGroupPendingSpinner(pendingSpinner)
    end

    local textWidth = math.ceil(widgets.comment:GetStringWidth() or 0)
    local statusIconLeftOffset = 10
    local iconAreaWidth = statusIconLeftOffset + APPLICATION_STATUS_ICON_SIZE + APPLICATION_STATUS_ICON_GAP
    local textLeftOffset = 2
    local textAreaWidth = availableWidth
    if statusFrame and math.floor((availableWidth - textWidth) / 2) < iconAreaWidth then
        textLeftOffset = textLeftOffset + iconAreaWidth
        textAreaWidth = math.max(1, availableWidth - iconAreaWidth)
    end
    if statusFrame then
        statusFrame:ClearAllPoints()
        statusFrame:SetPoint("LEFT", row, "LEFT", column.x + statusIconLeftOffset, 0)
        if applicationState == "pending" then
            StartFindGroupPendingSpinner(statusFrame)
        else
            statusFrame:Show()
        end
    end
    widgets.comment:SetPoint("LEFT", row, "LEFT", column.x + textLeftOffset, 0)
    widgets.comment:SetSize(textAreaWidth, 18)
    widgets.comment:SetJustifyH("CENTER")
end

local function LayoutResultType(row, hasIcon)
    local widgets = row and row.widgets
    if not (widgets and widgets.typeContainer and widgets.typeText and widgets.typeIcon) then
        return
    end

    local columnWidth = math.max(1, widgets.typeContainer:GetWidth() or 1)

    widgets.typeIcon:ClearAllPoints()
    widgets.typeText:ClearAllPoints()
    widgets.typeText:SetJustifyH("CENTER")
    widgets.typeText:SetPoint("LEFT", widgets.typeContainer, "LEFT", 0, 0)
    widgets.typeText:SetSize(columnWidth, 18)

    if hasIcon then
        widgets.typeIcon:SetSize(TYPE_ICON_SIZE, TYPE_ICON_SIZE)
        widgets.typeIcon:SetPoint("LEFT", widgets.typeContainer, "LEFT", TYPE_ICON_LEFT_OFFSET, 0)
        widgets.typeIcon:Show()
    else
        widgets.typeIcon:Hide()
    end
end

local function GetFindGroupHoverColor(resultType, applicationState)
    if applicationState == "cancelled" then
        return { 0.65, 0.65, 0.65, 0.18 }
    end
    if applicationState == "declined" or resultType == "leaver" then
        return { 1, 0.12, 0.08, 0.18 }
    end
    if resultType == "newbie" then
        return { 0.12, 1, 0.25, 0.16 }
    end
    if resultType == "leader" or resultType == "star" then
        return { 1, 0.82, 0, 0.16 }
    end
    if resultType == "bnet" or resultType == "guild" or resultType == "friend" then
        return { 0.35, 0.75, 1, 0.16 }
    end
    return { 1, 0.74, 0.18, 0.13 }
end

local function SetResultType(row, resultType, applicationState)
    local widgets = row.widgets
    widgets.typeIcon:Hide()
    widgets.typeText:SetText("")
    local backgroundTexture = ROW_TEXTURE_NORMAL
    local backgroundColor = { 1, 1, 1, 1 }
    local hasIcon = false
    if resultType == "leaver" then
        backgroundTexture = ROW_TEXTURE_RED
        widgets.typeIcon:SetTexture(LEAVER_ICON_TEXTURE)
        widgets.typeIcon:SetTexCoord(0, 1, 0, 1)
        widgets.typeText:SetText(TRANSLATE("逃兵"))
        widgets.typeText:SetTextColor(1, 0.08, 0.05, 1)
        hasIcon = true
    elseif resultType == "newbie" then
        backgroundTexture = ROW_TEXTURE_GREEN
        widgets.typeIcon:SetTexture(NEWBIE_ICON_TEXTURE)
        widgets.typeIcon:SetTexCoord(0, 1, 0, 1)
        widgets.typeText:SetText(TRANSLATE("新兵"))
        widgets.typeText:SetTextColor(0.12, 1, 0.25, 1)
        hasIcon = true
    elseif resultType == "leader" then
        widgets.typeIcon:SetTexture(LEADER_ICON_TEXTURE)
        widgets.typeIcon:SetTexCoord(0, 1, 0, 1)
        widgets.typeText:SetText(TRANSLATE("火车头"))
        widgets.typeText:SetTextColor(1, 0.82, 0, 1)
        hasIcon = true
    elseif resultType == "star" then
        widgets.typeIcon:SetTexture(STAR_ICON_TEXTURE)
        widgets.typeIcon:SetTexCoord(0, 1, 0, 1)
        widgets.typeText:SetText(TRANSLATE("星团长"))
        widgets.typeText:SetTextColor(1, 0.82, 0, 1)
        hasIcon = true
    elseif resultType == "bnet" then
        backgroundTexture = ROW_TEXTURE_BLUE
        widgets.typeIcon:SetTexture(BNET_ICON_TEXTURE)
        widgets.typeIcon:SetTexCoord(0, 1, 0, 1)
        widgets.typeText:SetText(TRANSLATE("战网好友"))
        widgets.typeText:SetTextColor(0.35, 0.75, 1, 1)
        hasIcon = true
    elseif resultType == "friend" then
        backgroundTexture = ROW_TEXTURE_BLUE
        widgets.typeIcon:SetTexture(BNET_ICON_TEXTURE)
        widgets.typeIcon:SetTexCoord(0, 1, 0, 1)
        widgets.typeText:SetText(TRANSLATE("好友"))
        widgets.typeText:SetTextColor(0.2, 1, 0.2, 1)
        hasIcon = true
    elseif resultType == "guild" then
        backgroundTexture = ROW_TEXTURE_BLUE
        widgets.typeIcon:SetTexture(BNET_ICON_TEXTURE)
        widgets.typeIcon:SetTexCoord(0, 1, 0, 1)
        widgets.typeText:SetText(TRANSLATE("公会"))
        widgets.typeText:SetTextColor(0.35, 0.75, 1, 1)
        hasIcon = true
    end
    if applicationState == "cancelled" or applicationState == "declined" then
        backgroundTexture = ROW_TEXTURE_GREY
        backgroundColor = { 1, 1, 1, 1 }
    end
    SetRowBackground(row, backgroundTexture, backgroundColor, true)
    SetRowHoverColor(row, GetFindGroupHoverColor(resultType, applicationState))
    LayoutResultType(row, hasIcon)
end

local function SetRoleEntries(row, entries)
    local widgets = row.widgets
    for index, icon in ipairs(widgets.roles or {}) do
        local entry = entries and entries[index]
        if entry then
            SetRoleIcon(icon, entry)
            icon:ClearAllPoints()
            icon:SetPoint("LEFT", widgets.rolesContainer, "LEFT", (index - 1) * (ROLE_ICON_SIZE + 2), 0)
            icon:Show()
        else
            icon:Hide()
        end
    end
end

local function FormatApplicationCountdown(seconds)
    seconds = math.max(0, math.floor(tonumber(seconds) or 0))
    return string.format("%d:%02d", math.floor(seconds / 60), seconds % 60)
end

local function RefreshRow(row, result, availableWidth, widgets, state, animateAppearance)
    row.mkFindGroupResult = result
    row.resultID = result.resultID
    if row.widgets.cancelApplicationButton then
        row.widgets.cancelApplicationButton.resultID = result.resultID
    end
    local applicationState = FindGroupSearch.GetApplicationDisplayState and FindGroupSearch.GetApplicationDisplayState(result) or nil
    ApplyRowLayout(row, availableWidth)
    SetResultType(row, FindGroupSearch.GetResultType(result), applicationState)
    SetRoleEntries(row, result.roleEntries)

    row.widgets.title:SetText(result.title or "-")
    row.widgets.title:SetTextColor(1, 1, 1, 1)
    row.widgets.dungeon:SetText(result.dungeonName or "-")
    row.widgets.dungeon:SetTextColor(1, 1, 1, 1)
    local requiredItemLevel = tonumber(result.requiredItemLevel) or 0
    row.widgets.requirement:SetText(requiredItemLevel > 0 and tostring(math.floor(requiredItemLevel + 0.5)) or "-")
    if requiredItemLevel > 0 then
        row.widgets.requirement:SetTextColor(REQUIREMENT_TEXT_COLOR[1], REQUIREMENT_TEXT_COLOR[2], REQUIREMENT_TEXT_COLOR[3], REQUIREMENT_TEXT_COLOR[4])
    else
        row.widgets.requirement:SetTextColor(REQUIREMENT_EMPTY_TEXT_COLOR[1], REQUIREMENT_EMPTY_TEXT_COLOR[2], REQUIREMENT_EMPTY_TEXT_COLOR[3], REQUIREMENT_EMPTY_TEXT_COLOR[4])
    end
    row.widgets.leader:SetText(result.leaderDisplayName or "-")
    local classColor = GetClassColor(result.leaderClassFile)
    if classColor then
        row.widgets.leader:SetTextColor(classColor.r, classColor.g, classColor.b, 1)
    else
        row.widgets.leader:SetTextColor(1, 1, 1, 1)
    end
    local score = tonumber(result.score) or 0
    row.widgets.score:SetText(score > 0 and tostring(math.floor(score + 0.5)) or "-")
    local scoreColor = GetScoreColor(score)
    row.widgets.score:SetTextColor(scoreColor.r or 1, scoreColor.g or 1, scoreColor.b or 1, 1)
    if applicationState == "pending" then
        local remainingSeconds = FindGroupSearch.GetApplicationRemainingSeconds and FindGroupSearch.GetApplicationRemainingSeconds(result) or 0
        row.widgets.comment:SetText(string.format(TRANSLATE("待定 %s"), FormatApplicationCountdown(remainingSeconds)))
        row.widgets.comment:SetTextColor(
            APPLICATION_PENDING_TEXT_COLOR[1],
            APPLICATION_PENDING_TEXT_COLOR[2],
            APPLICATION_PENDING_TEXT_COLOR[3],
            APPLICATION_PENDING_TEXT_COLOR[4]
        )
    elseif applicationState == "declined" then
        row.widgets.comment:SetText(TRANSLATE("被拒绝"))
        row.widgets.comment:SetTextColor(
            APPLICATION_DECLINED_TEXT_COLOR[1],
            APPLICATION_DECLINED_TEXT_COLOR[2],
            APPLICATION_DECLINED_TEXT_COLOR[3],
            APPLICATION_DECLINED_TEXT_COLOR[4]
        )
    elseif applicationState == "cancelled" then
        row.widgets.comment:SetText(TRANSLATE("已取消"))
        row.widgets.comment:SetTextColor(
            APPLICATION_CANCELLED_TEXT_COLOR[1],
            APPLICATION_CANCELLED_TEXT_COLOR[2],
            APPLICATION_CANCELLED_TEXT_COLOR[3],
            APPLICATION_CANCELLED_TEXT_COLOR[4]
        )
    else
        row.widgets.comment:SetText(result.comment or "")
        row.widgets.comment:SetTextColor(COMMENT_TEXT_COLOR[1], COMMENT_TEXT_COLOR[2], COMMENT_TEXT_COLOR[3], COMMENT_TEXT_COLOR[4])
    end
    if applicationState == "cancelled" then
        row.widgets.title:SetTextColor(APPLICATION_CANCELLED_TEXT_COLOR[1], APPLICATION_CANCELLED_TEXT_COLOR[2], APPLICATION_CANCELLED_TEXT_COLOR[3], APPLICATION_CANCELLED_TEXT_COLOR[4])
        row.widgets.dungeon:SetTextColor(APPLICATION_CANCELLED_TEXT_COLOR[1], APPLICATION_CANCELLED_TEXT_COLOR[2], APPLICATION_CANCELLED_TEXT_COLOR[3], APPLICATION_CANCELLED_TEXT_COLOR[4])
        row.widgets.typeText:SetTextColor(APPLICATION_CANCELLED_TEXT_COLOR[1], APPLICATION_CANCELLED_TEXT_COLOR[2], APPLICATION_CANCELLED_TEXT_COLOR[3], APPLICATION_CANCELLED_TEXT_COLOR[4])
        row.widgets.requirement:SetTextColor(APPLICATION_CANCELLED_TEXT_COLOR[1], APPLICATION_CANCELLED_TEXT_COLOR[2], APPLICATION_CANCELLED_TEXT_COLOR[3], APPLICATION_CANCELLED_TEXT_COLOR[4])
        row.widgets.leader:SetTextColor(APPLICATION_CANCELLED_TEXT_COLOR[1], APPLICATION_CANCELLED_TEXT_COLOR[2], APPLICATION_CANCELLED_TEXT_COLOR[3], APPLICATION_CANCELLED_TEXT_COLOR[4])
        row.widgets.score:SetTextColor(APPLICATION_CANCELLED_TEXT_COLOR[1], APPLICATION_CANCELLED_TEXT_COLOR[2], APPLICATION_CANCELLED_TEXT_COLOR[3], APPLICATION_CANCELLED_TEXT_COLOR[4])
    end
    LayoutApplicationComment(row, applicationState)
    row:Show()
    if result.reportRemovalPending == true then
        PlayRowFadeOut(row, result)
    else
        row.mkFindGroupRemovalResultID = nil
        row.mkFindGroupRemovalToken = nil
        StopAlphaAnimation(row, "remove", 1)
        PlayRowFadeIn(row, result.resultID, widgets, state, animateAppearance)
    end
end

local function EnsureRows(widgets, count)
    widgets.findGroupRows = widgets.findGroupRows or {}
    for index = #widgets.findGroupRows + 1, count do
        widgets.findGroupRows[index] = CreateResultRow(widgets.findGroupRowsFrame)
    end
end

local function UpdateFindGroupScrollState(widgets, rowCount)
    local scrollFrame = widgets and widgets.findGroupScrollFrame
    local content = widgets and widgets.findGroupRowsFrame
    local scrollBar = widgets and widgets.findGroupScrollBar
    if not (scrollFrame and content and scrollBar) then
        return
    end

    local scrollHeight = math.max(scrollFrame:GetHeight() or 0, 1)
    local contentHeight = math.max((rowCount or 0) * ROW_HEIGHT, scrollHeight)
    content:SetHeight(contentHeight)

    local maxScroll = math.max(0, contentHeight - scrollHeight)
    local currentValue = 0
    if scrollBar.mkGetScrollValue then
        currentValue = scrollBar.mkGetScrollValue() or 0
    elseif scrollBar.GetValue then
        currentValue = scrollBar:GetValue() or 0
    end
    currentValue = math.min(math.max(currentValue, 0), maxScroll)

    if scrollBar.mkSetScrollRange then
        scrollBar.mkSetScrollRange(0, maxScroll)
    elseif scrollBar.SetMinMaxValues then
        scrollBar:SetMinMaxValues(0, maxScroll)
    end

    if maxScroll > 0 then
        scrollBar:Show()
        SetFindGroupScrollValue(scrollFrame, scrollBar, currentValue)
        return
    end

    SetFindGroupScrollValue(scrollFrame, scrollBar, 0)
    scrollBar:Hide()
end

local function GetFindGroupScrollValue(widgets)
    local scrollBar = widgets and widgets.findGroupScrollBar
    if scrollBar and scrollBar.mkGetScrollValue then
        return scrollBar.mkGetScrollValue() or 0
    end
    local scrollFrame = widgets and widgets.findGroupScrollFrame
    if scrollFrame and scrollFrame.GetVerticalScroll then
        return scrollFrame:GetVerticalScroll() or 0
    end
    return 0
end

local function QueueVisibleResultDetails(owner, widgets, results)
    if not (FindGroupSearch.QueueResultDetails and widgets and widgets.findGroupScrollFrame) then
        return
    end
    local state = FindGroupSearch.GetState and FindGroupSearch.GetState(owner) or nil
    if not results then
        results = state and state.results or {}
    end
    local totalRows = #results
    if totalRows <= 0 then
        return
    end

    local scrollValue = math.max(0, GetFindGroupScrollValue(widgets))
    local firstIndex = math.floor(scrollValue / ROW_HEIGHT) + 1
    firstIndex = math.min(math.max(firstIndex, 1), totalRows)
    local visibleRows = math.ceil(math.max(widgets.findGroupScrollFrame:GetHeight() or ROW_HEIGHT, ROW_HEIGHT) / ROW_HEIGHT)
    visibleRows = math.min(math.max(visibleRows, 1), FIND_GROUP_VISIBLE_DETAIL_ROWS)
    local lastIndex = math.min(totalRows, firstIndex + visibleRows - 1)

    for index = firstIndex, lastIndex do
        local result = results[index]
        if result and result.resultID and result.detailLoaded ~= true then
            FindGroupSearch.QueueResultDetails(owner, result.resultID)
        end
    end
end

local function RenderRows(owner, widgets)
    if FindGroupSearch.SortResults then
        FindGroupSearch.SortResults(owner)
    end
    local state = FindGroupSearch.GetState(owner)
    local initialDisplayPending = state.initialDisplayPending == true and state.initialDisplayReady ~= true
    local results = initialDisplayPending and {} or (state.results or {})
    local totalRows = #results
    local isLoading = initialDisplayPending or state.searching or state.processing or state.newbieSearching
    EnsureRows(widgets, totalRows)

    local availableWidth = widgets.findGroupScrollFrame:GetWidth() - 12
    for index = 1, #widgets.findGroupRows do
        local row = widgets.findGroupRows[index]
        local result = results[index]
        if result then
            local previousResultID = tonumber(row.resultID)
            local currentResultID = tonumber(result.resultID)
            local wasShown = row.IsShown and row:IsShown()
            local animateAppearance = not wasShown
            if wasShown and previousResultID ~= currentResultID then
                StopAlphaAnimation(row, "appear", 1)
            end
            row:ClearAllPoints()
            row:SetPoint("TOPLEFT", widgets.findGroupRowsFrame, "TOPLEFT", 6, -((index - 1) * ROW_HEIGHT))
            row:SetPoint("TOPRIGHT", widgets.findGroupRowsFrame, "TOPRIGHT", -6, -((index - 1) * ROW_HEIGHT))
            RefreshRow(row, result, availableWidth, widgets, state, animateAppearance)
            SetFindGroupRowHover(row, row.IsMouseOver and row:IsMouseOver())
        else
            row:Hide()
            row.mkFindGroupResult = nil
            row.resultID = nil
            SetFindGroupRowHover(row, false)
            if row.widgets and row.widgets.cancelApplicationButtonHost then
                row.widgets.cancelApplicationButtonHost:Hide()
            end
            if row.widgets and row.widgets.cancelApplicationButton then
                row.widgets.cancelApplicationButton.resultID = nil
                row.widgets.cancelApplicationButton:Hide()
            end
            if row.widgets and row.widgets.applicationStatusIcon then
                row.widgets.applicationStatusIcon:Hide()
            end
            if row.widgets and row.widgets.applicationPendingSpinner then
                StopFindGroupPendingSpinner(row.widgets.applicationPendingSpinner)
            end
            StopAlphaAnimation(row, "appear", 1)
            StopAlphaAnimation(row, "remove", 1)
            row.mkFindGroupFadeKey = nil
            row.mkFindGroupRemovalResultID = nil
            row.mkFindGroupRemovalToken = nil
            row.mkFindGroupContextMenuLocked = nil
            row.mkFindGroupContextMenuToken = nil
        end
    end
    UpdateFindGroupScrollState(widgets, totalRows)
    QueueVisibleResultDetails(owner, widgets, results)

    if totalRows > 0 then
        widgets.findGroupEmpty:Hide()
        SetFindGroupLoadingAnimationShown(widgets, false, false)
    else
        local showLoading = isLoading
        local emptyText = state.lastError
            or isLoading and TRANSLATE("队伍列表检索中")
            or state.searchCompletedEmpty == true and TRANSLATE("未检索到队伍")
            or TRANSLATE("点击搜索获取队伍列表")
        if core.ApplyPanelEmptyPromptStyle then
            core.ApplyPanelEmptyPromptStyle(widgets.findGroupEmpty)
        else
            widgets.findGroupEmpty:SetTextColor(EMPTY_TEXT_COLOR[1], EMPTY_TEXT_COLOR[2], EMPTY_TEXT_COLOR[3], EMPTY_TEXT_COLOR[4])
        end
        widgets.findGroupEmpty:SetText(emptyText)
        UpdateFindGroupEmptyLayout(widgets, showLoading)
        widgets.findGroupEmpty:Show()
        SetFindGroupLoadingAnimationShown(widgets, showLoading, showLoading)
    end
end

local function AddRightHeader(panel, owner)
    local background = panel:CreateTexture(nil, "BACKGROUND", nil, 1)
    background:SetPoint("TOPLEFT", panel, "TOPLEFT", 0, HEADER_TOP_OFFSET)
    background:SetPoint("BOTTOMRIGHT", panel, "TOPRIGHT", 0, HEADER_BOTTOM_OFFSET)
    TrySetAtlas(background, "housefinder_header-bg-gradient", false)
    panel.HeaderBackground = background

    local header = CreateHeader(panel, owner)
    header:SetPoint("TOPLEFT", background, "TOPLEFT", HEADER_CONTENT_INSET_X, HEADER_CONTENT_OFFSET_Y)
    header:SetPoint("BOTTOMRIGHT", background, "BOTTOMRIGHT", -HEADER_CONTENT_INSET_X, HEADER_CONTENT_OFFSET_Y)
    header:SetFrameLevel(panel:GetFrameLevel() + 3)
    panel.HeaderFrame = header
    ApplyHeaderLayout(header, math.max((panel:GetWidth() or 0) - HEADER_CONTENT_INSET_X * 2, 1))
end

local function SetFindGroupRefreshIconPressed(button, pressed)
    local icon = button and button.Icon
    if not icon then
        return
    end

    icon:ClearAllPoints()
    if pressed then
        icon:SetSize(HEADER_REFRESH_ICON_PRESSED_SIZE, HEADER_REFRESH_ICON_PRESSED_SIZE)
        icon:SetPoint("CENTER", button, "CENTER", 1, -1)
        icon:SetAlpha(0.86)
    else
        icon:SetSize(HEADER_REFRESH_ICON_SIZE, HEADER_REFRESH_ICON_SIZE)
        icon:SetPoint("CENTER", button, "CENTER", 0, 0)
        icon:SetAlpha(1)
    end
end

local function SetFindGroupRefreshButtonEnabled(button, enabled)
    if not button then
        return
    end
    if enabled then
        button:Enable()
    else
        button:Disable()
        SetFindGroupRefreshIconPressed(button, false)
    end
    if button.SetAlpha then
        button:SetAlpha(enabled and 1 or 0.45)
    end
end

local function ApplyCurrentFindGroupSearchText(owner)
    local input = owner and owner.widgets and owner.widgets.findGroupSearchTextBox
    if input and FindGroupSearch.SetSearchTextFilter then
        FindGroupSearch.SetSearchTextFilter(owner, input:GetText() or "")
    end
end

local function TriggerFindGroupListRefresh(owner)
    ApplyCurrentFindGroupSearchText(owner)
    local ok, reason = FindGroupSearch.Search(owner)
    if not ok and owner and owner.Print then
        owner:Print(reason)
    end
    if owner and owner.RefreshMainWindow then
        owner:RefreshMainWindow("FindGroupListRefresh")
    end
end

local function CreateFindGroupListRefreshButton(panel, owner, scrollBar)
    local button = CreateFrame("Button", nil, panel)
    button:SetSize(HEADER_REFRESH_BUTTON_SIZE, HEADER_REFRESH_BUTTON_SIZE)
    button:SetFrameLevel((panel:GetFrameLevel() or 0) + 6)
    button:RegisterForClicks("LeftButtonUp")
    if scrollBar then
        button:SetPoint("BOTTOM", scrollBar, "TOP", 0, HEADER_REFRESH_BUTTON_OFFSET_Y)
    else
        button:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -HEADER_CONTENT_INSET_X, HEADER_TOP_OFFSET)
    end

    local icon = button:CreateTexture(nil, "OVERLAY")
    icon:SetTexture(REFRESH_BUTTON_TEXTURE)
    icon:SetTexCoord(0, 1, 0, 1)
    button.Icon = icon
    SetFindGroupRefreshIconPressed(button, false)

    button:SetScript("OnClick", function()
        TriggerFindGroupListRefresh(owner)
    end)
    button:SetScript("OnEnter", function(hoveredButton)
        GameTooltip:SetOwner(hoveredButton, "ANCHOR_RIGHT")
        if GameTooltip_SetTitle then
            GameTooltip_SetTitle(GameTooltip, TRANSLATE("刷新队伍列表"))
        else
            GameTooltip:SetText(TRANSLATE("刷新队伍列表"), 1, 0.82, 0, true)
        end
        GameTooltip:AddLine(TRANSLATE("重新搜索寻找队伍列表"), 1, 1, 1, true)
        GameTooltip:Show()
    end)
    button:SetScript("OnMouseDown", function(clickedButton, mouseButton)
        if mouseButton == "LeftButton" then
            SetFindGroupRefreshIconPressed(clickedButton, true)
        end
    end)
    button:SetScript("OnMouseUp", function(clickedButton)
        SetFindGroupRefreshIconPressed(clickedButton, false)
    end)
    button:SetScript("OnLeave", function(hoveredButton)
        SetFindGroupRefreshIconPressed(hoveredButton, false)
        if GameTooltip:GetOwner() == hoveredButton then
            GameTooltip:Hide()
        end
    end)
    button:SetScript("OnDisable", function(disabledButton)
        SetFindGroupRefreshIconPressed(disabledButton, false)
    end)

    SetFindGroupRefreshButtonEnabled(button, FindGroupSearch.HasSelectedDungeons and FindGroupSearch.HasSelectedDungeons(owner))
    return button
end

local function CreateRightPanel(owner, container, leftPanel)
    local panel = CreateFrame("Frame", nil, container, "BackdropTemplate")
    panel:SetPoint("TOPLEFT", leftPanel, "TOPRIGHT", RIGHT_PANEL_OVERLAP, 0)
    panel:SetPoint("BOTTOMRIGHT", container, "BOTTOMRIGHT", 0, 0)

    local rightBackground = panel:CreateTexture(nil, "BACKGROUND")
    rightBackground:SetAllPoints(panel)
    rightBackground:SetTexture("Interface\\Buttons\\WHITE8X8")
    rightBackground:SetVertexColor(0, 0, 0, 1)
    rightBackground:Hide()
    panel.Background = rightBackground

    AddRightHeader(panel, owner)

    local scrollFrame, rowsFrame, scrollBar = CreateFindGroupScrollContainer(panel)
    scrollFrame:SetPoint("TOPLEFT", panel.HeaderBackground, "BOTTOMLEFT", 0, -4)
    scrollFrame:SetPoint(
        "BOTTOMRIGHT",
        panel,
        "BOTTOMRIGHT",
        -(APPLICANT_SCROLLBAR_WIDTH + APPLICANT_SCROLLBAR_GAP + APPLICANT_SCROLLBAR_RIGHT_INSET),
        APPLICANT_SCROLL_BOTTOM_INSET
    )
    scrollFrame:SetFrameLevel(panel:GetFrameLevel() + 2)
    rowsFrame:SetFrameLevel(scrollFrame:GetFrameLevel() + 1)
    scrollBar:SetFrameLevel(scrollFrame:GetFrameLevel() + 2)
    scrollBar:SetPoint("TOPLEFT", scrollFrame, "TOPRIGHT", APPLICANT_SCROLLBAR_GAP, -2)
    scrollBar:SetPoint("BOTTOMLEFT", scrollFrame, "BOTTOMRIGHT", APPLICANT_SCROLLBAR_GAP, 2)
    panel.RefreshButton = CreateFindGroupListRefreshButton(panel, owner, scrollBar)
    if scrollFrame.SetClipsChildren then
        scrollFrame:SetClipsChildren(true)
    end
    scrollFrame.mkOnScrollValueChanged = function()
        if owner.widgets and owner.activeTab == "findgroup" then
            QueueVisibleResultDetails(owner, owner.widgets)
        end
    end

    local empty = rowsFrame:CreateFontString(nil, "OVERLAY", "GameFontDisable")
    empty:SetPoint("CENTER", rowsFrame, "CENTER", 0, 0)
    empty:SetJustifyH("CENTER")
    empty:SetJustifyV("MIDDLE")
    empty:SetText(TRANSLATE("点击搜索获取队伍列表"))
    if core.ApplyPanelEmptyPromptStyle then
        core.ApplyPanelEmptyPromptStyle(empty)
    end

    panel:SetScript("OnSizeChanged", function()
        if panel.HeaderFrame then
            local headerWidth = scrollFrame and scrollFrame.GetWidth and scrollFrame:GetWidth()
            if not headerWidth or headerWidth <= 0 then
                headerWidth = (panel:GetWidth() or 0) - (HEADER_CONTENT_INSET_X * 2)
            end
            ApplyHeaderLayout(panel.HeaderFrame, math.max(headerWidth - 12, 1))
        end
    end)
    scrollFrame:HookScript("OnSizeChanged", function()
        if owner.widgets and owner.activeTab == "findgroup" then
            ApplyHeaderLayout(panel.HeaderFrame, math.max(scrollFrame:GetWidth() - 12, 1))
            RenderRows(owner, owner.widgets)
        end
    end)

    local topRightFiligree = panel:CreateTexture(nil, "ARTWORK", nil, 5)
    topRightFiligree:SetPoint("TOPRIGHT", panel, "TOPRIGHT", TOP_RIGHT_FILIGREE_OFFSET_X, TOP_RIGHT_FILIGREE_OFFSET_Y)
    TrySetAtlas(topRightFiligree, "housing-dashboard-filigree-wood-small-TR", true)
    panel.TopRightFiligree = topRightFiligree

    local bottomRightFiligree = panel:CreateTexture(nil, "ARTWORK", nil, 5)
    bottomRightFiligree:SetPoint("BOTTOMRIGHT", panel, "BOTTOMRIGHT", BOTTOM_RIGHT_FILIGREE_OFFSET_X, BOTTOM_RIGHT_FILIGREE_OFFSET_Y)
    TrySetAtlas(bottomRightFiligree, "housing-dashboard-filigree-wood-small-BR", true)
    panel.BottomRightFiligree = bottomRightFiligree

    return panel, {
        findGroupHeader = panel.HeaderFrame,
        findGroupScrollFrame = scrollFrame,
        findGroupRowsFrame = rowsFrame,
        findGroupScrollBar = scrollBar,
        findGroupListRefreshButton = panel.RefreshButton,
        findGroupEmpty = empty,
        findGroupRows = {},
    }
end

function FindGroupTab.HandleRowClick(row)
    local owner = MK
    local result = row and row.mkFindGroupResult
    if not (owner and result) then
        return
    end
    local settings = FindGroupSearch.GetSettings(owner) or {}
    if settings.quickJoin ~= true then
        return
    end
    local now = GetTime and GetTime() or time()
    if owner.mkFindGroupLastClickedResultID == result.resultID and now - (owner.mkFindGroupLastClickAt or 0) <= 0.35 then
        if FindGroupSearch.CanApplyToResult and not FindGroupSearch.CanApplyToResult(owner, result.resultID) then
            owner.mkFindGroupLastClickedResultID = nil
            owner.mkFindGroupLastClickAt = nil
            return
        end
        local ok, reason = FindGroupSearch.ApplyToResult(owner, result.resultID)
        if ok then
            owner:RefreshMainWindow("FindGroupApply", tostring(result.resultID))
        elseif reason and owner.Print then
            owner:Print(reason)
        end
    end
    owner.mkFindGroupLastClickedResultID = result.resultID
    owner.mkFindGroupLastClickAt = now
end

function FindGroupTab.ShowContextMenu(row)
    local owner = MK
    local result = row and row.mkFindGroupResult
    if not (owner and result and MenuUtil) then
        return
    end
    local menuToken = LockFindGroupRowContextMenu(row)
    local menu = MenuUtil.CreateContextMenu(row, function(_, rootDescription)
        rootDescription:SetTag("MENU_MK_FIND_GROUP_RESULT")
        rootDescription:CreateTitle(result.title or "")
        local canApply = not FindGroupSearch.CanApplyToResult or FindGroupSearch.CanApplyToResult(owner, result.resultID)
        local applyText = canApply
            and (APPLICATION_MENU_APPLY_COLOR_CODE .. TRANSLATE("申请加入") .. "|r")
            or TRANSLATE("申请加入")
        local applyButton = rootDescription:CreateButton(applyText, function()
            local ok, reason = FindGroupSearch.OpenApplicationDialog(owner, result.resultID)
            if not ok and owner.Print then
                owner:Print(reason)
            end
        end)
        if not canApply and applyButton and applyButton.SetEnabled then
            applyButton:SetEnabled(false)
        end
        rootDescription:CreateButton(TRANSLATE("与队长密聊"), function()
            if result.leaderName and ChatFrameUtil and ChatFrameUtil.SendTell then
                ChatFrameUtil.SendTell(result.leaderName)
            elseif result.leaderName and ChatFrame_SendTell then
                ChatFrame_SendTell(result.leaderName)
            end
        end)
        rootDescription:CreateButton(TRANSLATE("举报广告"), function()
            local ok = FindGroupSearch.ReportAdvertisement and FindGroupSearch.ReportAdvertisement(owner, result)
            if not ok and owner.Print then
                owner:Print(TRANSLATE("举报接口不可用。"))
            end
        end)
        rootDescription:CreateButton(TRANSLATE("屏蔽同标题广告菜单"), function()
            FindGroupSearch.BlockTitle(owner, result.title, result)
            owner:RefreshMainWindow("FindGroupBlockTitle")
        end)
        local blacklistButton = rootDescription:CreateButton(BLACKLIST_MENU_ICON_MARKUP .. BLACKLIST_MENU_COLOR_CODE .. TRANSLATE("加入黑名单") .. "|r", function()
            if owner.AddPlayerToBlacklist and result.leaderName then
                local ok, record = owner:AddPlayerToBlacklist(
                    result.leaderName,
                    nil,
                    owner.GetBlacklistNoteManual and owner:GetBlacklistNoteManual() or "手动拉黑",
                    "findgroup_menu"
                )
                if ok and FindGroupSearch.RefreshBlacklistFilter then
                    FindGroupSearch.RefreshBlacklistFilter(owner)
                end
                if ok and owner.PrintBlacklistAdded then
                    owner:PrintBlacklistAdded(record)
                end
                owner:RefreshMainWindow("FindGroupBlacklistLeader")
            end
        end)
        if not result.leaderName and blacklistButton and blacklistButton.SetEnabled then
            blacklistButton:SetEnabled(false)
        end
    end)
    if menu and menu.SetClosedCallback then
        menu:SetClosedCallback(function()
            FinishFindGroupRowContextMenu(row, menuToken)
        end)
    else
        FinishFindGroupRowContextMenu(row, menuToken)
    end
end

function FindGroupTab.ShowTooltip(row)
    local result = row and row.mkFindGroupResult
    if not result then
        return
    end
    if FindGroupSearch.EnsureResultDetails then
        result = FindGroupSearch.EnsureResultDetails(MK, result.resultID) or result
        row.mkFindGroupResult = result
        if result.detailLoaded == true then
            local applicationState = FindGroupSearch.GetApplicationDisplayState and FindGroupSearch.GetApplicationDisplayState(result) or nil
            SetResultType(row, FindGroupSearch.GetResultType(result), applicationState)
            SetRoleEntries(row, result.roleEntries)
        end
    end

    local tooltip = GameTooltip
    tooltip:SetOwner(row, "ANCHOR_RIGHT")
    tooltip:ClearLines()
    tooltip:AddLine(result.title or "-", 1, 1, 1)
    tooltip:AddLine(result.dungeonName or "-", 0.2, 1, 0.2)
    tooltip:AddLine(" ")
    tooltip:AddDoubleLine(TRANSLATE("队长："), ColorText(result.leaderDisplayName or "-", GetClassColor(result.leaderClassFile)), 1, 0.82, 0, 1, 1, 1)
    tooltip:AddDoubleLine(TRANSLATE("队长大秘评分："), ColorText(math.floor((tonumber(result.score) or 0) + 0.5), GetScoreColor(result.score)), 1, 0.82, 0, 1, 1, 1)
    tooltip:AddDoubleLine(TRANSLATE("队员："), string.format("%d (%d/%d/%d)", result.totalMembers or 0, tonumber(result.counts.TANK) or 0, tonumber(result.counts.HEALER) or 0, tonumber(result.counts.DAMAGER) or 0), 1, 0.82, 0, 1, 1, 1)
    tooltip:AddDoubleLine(TRANSLATE("空位："), tostring(result.availableSlots or 0), 1, 0.82, 0, 1, 1, 1)
    if tonumber(result.requiredItemLevel) and result.requiredItemLevel > 0 then
        tooltip:AddDoubleLine(TRANSLATE("需要物品等级："), tostring(math.floor(result.requiredItemLevel + 0.5)), 1, 0.82, 0, 1, 1, 1)
    end
    if tonumber(result.age) then
        tooltip:AddDoubleLine(TRANSLATE("创建于："), string.format(TRANSLATE("%d 分钟前"), math.max(0, math.floor(result.age / 60))), 1, 0.82, 0, 1, 1, 1)
    end

    local newbieNames = {}
    local leaverNames = {}
    local leaderNames = {}
    local starNames = {}
    if #result.members > 0 then
        tooltip:AddLine(" ")
        tooltip:AddLine(TRANSLATE("队伍成员"), 1, 0.82, 0)
        local shownMembers = 0
        for _, member in ipairs(result.members) do
            local memberName = member.displayName or member.name
            if not IsUnknownMemberName(memberName) then
                local roleKey = member.role and (member.role == "HEALER" and "HEAL" or member.role == "DAMAGER" and "DPS" or member.role)
                local roleText = TRANSLATE(ROLE_TEXT_KEY_BY_ROLE[roleKey] or ROLE_TEXT_KEY_BY_ROLE.UNKNOWN)
                local specText = member.specName and member.specName ~= "" and (" - " .. member.specName) or ""
                local classColor = GetClassColor(member.classFile)
                tooltip:AddLine(string.format("%s  %s%s", roleText, ColorText(memberName, classColor), specText), 1, 1, 1)
                shownMembers = shownMembers + 1
                local formattedName = GetTooltipMemberName(member)
                if member.isLeaver == true and formattedName then
                    leaverNames[#leaverNames + 1] = formattedName
                end
                if member.isNewbie == true and formattedName then
                    newbieNames[#newbieNames + 1] = formattedName
                end
                if member.isAccountTypeLeader == true and formattedName then
                    leaderNames[#leaderNames + 1] = formattedName
                end
                if member.isAccountTypeStar == true and formattedName then
                    starNames[#starNames + 1] = formattedName
                end
            end
        end
        if shownMembers <= 0 then
            tooltip:AddLine(TRANSLATE("成员信息加载中"), 0.7, 0.7, 0.7, true)
        end
    elseif result.detailLoaded ~= true and (tonumber(result.totalMembers) or 0) > 0 then
        tooltip:AddLine(" ")
        tooltip:AddLine(TRANSLATE("成员信息加载中"), 0.7, 0.7, 0.7, true)
    end
    local resultType = FindGroupSearch.GetResultType(result)
    local hasTypeWarning = false
    if resultType == "leaver" or #leaverNames > 0 then
        tooltip:AddLine(" ")
        hasTypeWarning = AddTypeMemberTooltipLine(
            tooltip,
            LEAVER_ICON_TEXTURE,
            TRANSLATE("逃兵"),
            #leaverNames > 0 and leaverNames or { TRANSLATE("成员信息加载中") },
            1,
            0.08,
            0.05
        )
    end
    if result.hasNewbie == true or resultType == "newbie" then
        if not hasTypeWarning then
            tooltip:AddLine(" ")
            hasTypeWarning = true
        end
        AddTypeMemberTooltipLine(
            tooltip,
            NEWBIE_ICON_TEXTURE,
            TRANSLATE("新兵"),
            #newbieNames > 0 and newbieNames or { TRANSLATE("成员信息加载中") },
            0.12,
            1,
            0.25
        )
    end
    if result.hasAccountTypeLeader == true or resultType == "leader" then
        if not hasTypeWarning then
            tooltip:AddLine(" ")
            hasTypeWarning = true
        end
        AddTypeMemberTooltipLine(
            tooltip,
            LEADER_ICON_TEXTURE,
            TRANSLATE("火车头"),
            #leaderNames > 0 and leaderNames or { TRANSLATE("成员信息加载中") },
            1,
            0.82,
            0
        )
    end
    if result.hasAccountTypeStar == true or resultType == "star" then
        if not hasTypeWarning then
            tooltip:AddLine(" ")
            hasTypeWarning = true
        end
        AddTypeMemberTooltipLine(
            tooltip,
            STAR_ICON_TEXTURE,
            TRANSLATE("星团长"),
            #starNames > 0 and starNames or { TRANSLATE("成员信息加载中") },
            1,
            0.82,
            0
        )
    end
    if result.comment and result.comment ~= "" then
        tooltip:AddLine(" ")
        tooltip:AddLine(result.comment, GREEN_FONT_COLOR.r, GREEN_FONT_COLOR.g, GREEN_FONT_COLOR.b, true)
    end
    tooltip:Show()
end

function FindGroupList.CreateRightPanel(owner, container, leftPanel)
    return CreateRightPanel(owner, container, leftPanel)
end

function FindGroupList.Refresh(owner, widgets)
    if not widgets then
        return
    end
    if widgets.findGroupHeader and widgets.findGroupScrollFrame then
        RefreshHeaderSortState(widgets.findGroupHeader, owner)
        ApplyHeaderLayout(widgets.findGroupHeader, math.max(widgets.findGroupScrollFrame:GetWidth() - 12, 1))
    end
    SetFindGroupRefreshButtonEnabled(
        widgets.findGroupListRefreshButton,
        FindGroupSearch.HasSelectedDungeons and FindGroupSearch.HasSelectedDungeons(owner)
    )
    RenderRows(owner, widgets)
end
