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

core.FindGroupTab = core.FindGroupTab or {}
local FindGroupTab = core.FindGroupTab
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 CreateLabel = MeetingStoneForm.CreateLabel
local CreateInput = MeetingStoneForm.CreateInput
local StyleInputBox = MeetingStoneForm.StyleInputBox
local CreateSystemButton = MeetingStoneForm.CreateSystemButton
local RefreshDropdown = MeetingStoneForm.RefreshDropdown or function(dropdown)
    if dropdown and dropdown.GenerateMenu then
        dropdown:GenerateMenu()
    end
end
local EnableSelectAllAfterMouseLeave = MeetingStoneForm.EnableSelectAllAfterMouseLeave or function() end

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 MKA_ICON_PATH = core.MKA_ICON_PATH or ("Interface\\AddOns\\" .. addonName .. "\\Art\\Ui\\Icon\\")
local ROW_TEXTURE_NORMAL = "Interface\\AddOns\\" .. addonName .. "\\Art\\Rows\\ApplicantRowNormal.png"
local ROW_TEXTURE_GREEN = "Interface\\AddOns\\" .. addonName .. "\\Art\\Rows\\ApplicantRowGreen.png"
local ROW_TEXTURE_RED = "Interface\\AddOns\\" .. addonName .. "\\Art\\Rows\\ApplicantRowRed.png"
local ROW_TEXTURE_BLUE = "Interface\\AddOns\\" .. addonName .. "\\Art\\Rows\\ApplicantRowBlue.png"
local ROW_TEXTURE_GREY = "Interface\\AddOns\\" .. addonName .. "\\Art\\Rows\\ApplicantRowGrey.png"
local LEAVER_ICON_TEXTURE = MKA_ICON_PATH .. "isLeaver.png"
local NEWBIE_ICON_TEXTURE = MKA_ICON_PATH .. "Newbie.png"
local BNET_ICON_TEXTURE = MKA_ICON_PATH .. "BattleNet.png"
local APPLICATION_ALERT_ICON_TEXTURE = "Interface\\DialogFrame\\UI-Dialog-Icon-AlertNew.blp"

local PANEL_INSET_LEFT = MeetingStoneConstants.MEETINGSTONE_PANEL_INSET_LEFT or 5
local PANEL_INSET_TOP = MeetingStoneConstants.MEETINGSTONE_PANEL_INSET_TOP or 0
local PANEL_INSET_RIGHT = MeetingStoneConstants.MEETINGSTONE_PANEL_INSET_RIGHT or 4
local PANEL_INSET_BOTTOM = MeetingStoneConstants.MEETINGSTONE_PANEL_INSET_BOTTOM or 8
local LEFT_PANEL_WIDTH = MeetingStoneConstants.MEETINGSTONE_LEFT_PANEL_WIDTH or 180
local RIGHT_PANEL_OVERLAP = MeetingStoneConstants.MEETINGSTONE_RIGHT_PANEL_OVERLAP or -4
local LEFT_PANEL_BACKGROUND_ALPHA = MeetingStoneConstants.MEETINGSTONE_LEFT_PANEL_BACKGROUND_ALPHA or 0.6
local PANEL_DIVIDER_WIDTH = MeetingStoneConstants.MEETINGSTONE_PANEL_DIVIDER_WIDTH or 3
local PANEL_DIVIDER_OFFSET_X = MeetingStoneConstants.MEETINGSTONE_PANEL_DIVIDER_OFFSET_X or -3
local PANEL_DIVIDER_TOP_OFFSET = MeetingStoneConstants.MEETINGSTONE_PANEL_DIVIDER_TOP_OFFSET or 4
local PANEL_DIVIDER_BOTTOM_OFFSET = MeetingStoneConstants.MEETINGSTONE_PANEL_DIVIDER_BOTTOM_OFFSET or 2
local PANEL_DIVIDER_COLOR = MeetingStoneConstants.MEETINGSTONE_PANEL_DIVIDER_COLOR or { 0.38, 0.34, 0.24, 0.9 }
local PANEL_DIVIDER_HIGHLIGHT_COLOR = MeetingStoneConstants.MEETINGSTONE_PANEL_DIVIDER_HIGHLIGHT_COLOR or { 0.62, 0.55, 0.38, 0.3 }
local PANEL_DIVIDER_SHADOW_COLOR = MeetingStoneConstants.MEETINGSTONE_PANEL_DIVIDER_SHADOW_COLOR or { 0.08, 0.07, 0.05, 0.9 }
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 HEADER_TEXT_SIZE = MeetingStoneConstants.MEETINGSTONE_HEADER_TEXT_SIZE or 14
local FORM_INSET_X = MeetingStoneConstants.MEETINGSTONE_FORM_INSET_X or 15
local FORM_TOP_OFFSET = MeetingStoneConstants.MEETINGSTONE_FORM_TOP_OFFSET or -52
local FORM_LABEL_TO_CONTROL_GAP = MeetingStoneConstants.MEETINGSTONE_FORM_LABEL_TO_CONTROL_GAP or 8
local FORM_CONTROL_WIDTH = MeetingStoneConstants.MEETINGSTONE_FORM_CONTROL_WIDTH or 150
local FORM_CONTROL_HEIGHT = MeetingStoneConstants.MEETINGSTONE_FORM_CONTROL_HEIGHT or 26
local FORM_ROW_GAP = MeetingStoneConstants.MEETINGSTONE_FORM_ROW_GAP or 8
local FORM_INLINE_LABEL_WIDTH = MeetingStoneConstants.MEETINGSTONE_FORM_INLINE_LABEL_WIDTH or 74
local FORM_INLINE_INPUT_WIDTH = MeetingStoneConstants.MEETINGSTONE_FORM_INLINE_INPUT_WIDTH or 70
local FORM_BUTTON_WIDTH = MeetingStoneConstants.MEETINGSTONE_FORM_BUTTON_WIDTH or 72
local FORM_BUTTON_HEIGHT = MeetingStoneConstants.MEETINGSTONE_FORM_BUTTON_HEIGHT or 26
local FORM_BUTTON_GAP = MeetingStoneConstants.MEETINGSTONE_FORM_BUTTON_GAP or 7
local FORM_BUTTON_BOTTOM_OFFSET = MeetingStoneConstants.MEETINGSTONE_FORM_BUTTON_BOTTOM_OFFSET or 8
local ACTION_BUTTON_TEXT_OFFSET_Y = MeetingStoneConstants.MEETINGSTONE_ACTION_BUTTON_TEXT_OFFSET_Y or 1
local LEFT_BOTTOM_GRADIENT_TOP_OFFSET = MeetingStoneConstants.MEETINGSTONE_LEFT_BOTTOM_GRADIENT_TOP_OFFSET or 32
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 FIND_GROUP_MIN_AVAILABLE_SLOTS = 1
local FIND_GROUP_MAX_AVAILABLE_SLOTS = 4
local FIND_GROUP_SLOT_SPINNER_INPUT_WIDTH = FORM_INLINE_INPUT_WIDTH + 16
local FIND_GROUP_SLOT_SPINNER_INPUT_HEIGHT = FORM_CONTROL_HEIGHT
local FIND_GROUP_SLOT_SPINNER_BUTTON_WIDTH = 23
local FIND_GROUP_SLOT_SPINNER_BUTTON_HEIGHT = 22
local FIND_GROUP_SLOT_SPINNER_BUTTON_INSET = 6
FindGroupTab.SLOT_SPINNER_BUTTON_EDGE_OFFSET = 0
local ROW_SIDE_INSET = 4
local ROW_GAP = 1
local ROLE_ICON_SIZE = 16
local TYPE_ICON_SIZE = 16
local TYPE_TEXT_MIN_WIDTH = 54
local TYPE_COLUMN_MIN_WIDTH = TYPE_ICON_SIZE + 4 + TYPE_TEXT_MIN_WIDTH + 12
local TYPE_TEXT_PADDING = 12
local TITLE_COLUMN_MIN_WIDTH = 86
local HEADER_NORMAL_TEXT_COLOR = { 1, 0.82, 0, 1 }
local HEADER_SORTED_TEXT_COLOR = { 1, 1, 1, 1 }
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 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 = { 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 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 = "FIND_GROUP.HEADER.TITLE",
    dungeon = "FIND_GROUP.HEADER.DUNGEON",
    type = "FIND_GROUP.HEADER.TYPE",
    members = "FIND_GROUP.HEADER.MEMBERS",
    requirement = "FIND_GROUP.HEADER.REQUIREMENT",
    leader = "FIND_GROUP.HEADER.LEADER",
    score = "FIND_GROUP.HEADER.SCORE",
    comment = "FIND_GROUP.HEADER.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("FIND_GROUP.TOOLTIP.TYPE_MEMBERS_FORMAT"),
        label,
        table.concat(names, TRANSLATE("FIND_GROUP.TOOLTIP.NAME_SEPARATOR"))
    )
    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 CreateFindGroupLabel(parent, text)
    if CreateLabel then
        return CreateLabel(parent, text)
    end
    local label = CreatePlainLabel(parent, text, 14, "LEFT")
    label:SetTextColor(1, 0.82, 0, 1)
    return label
end

local function CreateFindGroupInput(parent, width, height, numeric)
    if CreateInput then
        return CreateInput(parent, width, height, numeric)
    end
    local editBox = CreateFrame("EditBox", nil, parent, "InputBoxTemplate")
    editBox:SetSize(width, height or 24)
    editBox:SetAutoFocus(false)
    if numeric then
        editBox:SetNumeric(true)
    end
    return editBox
end

local function CreateFindGroupStepperButton(parent, direction)
    local button = CreateFrame("Button", nil, parent)
    button:SetSize(FIND_GROUP_SLOT_SPINNER_BUTTON_WIDTH, FIND_GROUP_SLOT_SPINNER_BUTTON_HEIGHT)
    if direction == "left" then
        button:SetNormalTexture("Interface\\Buttons\\UI-SpellbookIcon-PrevPage-Up")
        button:SetPushedTexture("Interface\\Buttons\\UI-SpellbookIcon-PrevPage-Down")
        button:SetDisabledTexture("Interface\\Buttons\\UI-SpellbookIcon-PrevPage-Disabled")
    else
        button:SetNormalTexture("Interface\\Buttons\\UI-SpellbookIcon-NextPage-Up")
        button:SetPushedTexture("Interface\\Buttons\\UI-SpellbookIcon-NextPage-Down")
        button:SetDisabledTexture("Interface\\Buttons\\UI-SpellbookIcon-NextPage-Disabled")
    end
    button:SetHighlightTexture("Interface\\Buttons\\UI-Common-MouseHilight", "ADD")
    button:RegisterForClicks("LeftButtonUp")
    return button
end

local function CreateFindGroupNumericSpinner(parent, minValue, maxValue)
    local spinner = CreateFrame("Frame", nil, parent)
    spinner.mkaFindGroupUsesNativeSpinner = true
    spinner.mkaFindGroupMinValue = minValue
    spinner.mkaFindGroupMaxValue = maxValue
    spinner:SetSize(FIND_GROUP_SLOT_SPINNER_INPUT_WIDTH, FIND_GROUP_SLOT_SPINNER_INPUT_HEIGHT)

    local inputWidth = math.max(
        1,
        FIND_GROUP_SLOT_SPINNER_INPUT_WIDTH - (FIND_GROUP_SLOT_SPINNER_BUTTON_WIDTH * 2)
    )
    local editBox = CreateFrame("EditBox", nil, spinner)
    editBox:SetSize(inputWidth, FIND_GROUP_SLOT_SPINNER_INPUT_HEIGHT)
    editBox:SetPoint("CENTER", spinner, "CENTER", 0, 0)
    editBox:EnableMouse(true)
    editBox:SetAutoFocus(false)
    editBox:SetNumeric(true)
    editBox:SetMaxLetters(1)
    editBox:SetJustifyH("CENTER")
    editBox:SetTextColor(1, 0.96, 0.86, 1)
    editBox:SetShadowColor(0, 0, 0, 0.8)
    editBox:SetShadowOffset(1, -1)
    if core.ApplyFont then
        core.ApplyFont(editBox, 14, "")
    else
        editBox:SetFontObject("GameFontHighlight")
    end

    local decrementButton = CreateFindGroupStepperButton(spinner, "left")
    decrementButton:SetPoint(
        "LEFT",
        spinner,
        "LEFT",
        FIND_GROUP_SLOT_SPINNER_BUTTON_INSET - FindGroupTab.SLOT_SPINNER_BUTTON_EDGE_OFFSET,
        0
    )
    local incrementButton = CreateFindGroupStepperButton(spinner, "right")
    incrementButton:SetPoint(
        "RIGHT",
        spinner,
        "RIGHT",
        -FIND_GROUP_SLOT_SPINNER_BUTTON_INSET + FindGroupTab.SLOT_SPINNER_BUTTON_EDGE_OFFSET,
        0
    )

    spinner.mkaFindGroupEditBox = editBox
    spinner.DecrementButton = decrementButton
    spinner.IncrementButton = incrementButton

    local function clampValue(value)
        local numberValue = tonumber(value) or minValue or 0
        numberValue = math.floor(numberValue)
        return math.max(
            tonumber(spinner.mkaFindGroupMinValue) or -math.huge,
            math.min(tonumber(spinner.mkaFindGroupMaxValue) or math.huge, numberValue)
        )
    end

    function spinner:SetValue(value)
        local newValue = clampValue(value)
        local changed = newValue ~= self.currentValue
        self.currentValue = newValue
        self.mkaFindGroupEditBox.mkaFindGroupProgrammaticSetValue = true
        if self.mkaFindGroupEditBox:GetText() ~= tostring(newValue) then
            self.mkaFindGroupEditBox:SetText(tostring(newValue))
        end
        self.mkaFindGroupEditBox.mkaFindGroupProgrammaticSetValue = nil
        if changed and self.onValueChangedCallback and not self.mkaFindGroupProgrammaticSetValue then
            self.onValueChangedCallback(self, newValue)
        end
    end

    function spinner:SetNumber(value)
        self:SetValue(value)
    end

    function spinner:GetNumber()
        return self.currentValue or clampValue(self.mkaFindGroupEditBox:GetNumber())
    end

    function spinner:GetText()
        return self.mkaFindGroupEditBox:GetText()
    end

    function spinner:SetText(text)
        self:SetValue(text)
    end

    function spinner:SetOnValueChangedCallback(callback)
        self.onValueChangedCallback = callback
    end

    editBox:SetScript("OnTextChanged", function(input)
        if input.mkaFindGroupProgrammaticSetValue then
            return
        end
        spinner:SetValue(input:GetNumber())
    end)
    editBox:SetScript("OnEnterPressed", function(input)
        input:ClearFocus()
    end)
    editBox:SetScript("OnEscapePressed", function(input)
        input:ClearFocus()
    end)
    decrementButton:SetScript("OnClick", function()
        spinner:SetValue(spinner:GetNumber() - 1)
    end)
    incrementButton:SetScript("OnClick", function()
        spinner:SetValue(spinner:GetNumber() + 1)
    end)
    spinner:EnableMouseWheel(true)
    spinner:SetScript("OnMouseWheel", function(self, delta)
        if delta > 0 then
            self:SetValue(self:GetNumber() + 1)
        elseif delta < 0 then
            self:SetValue(self:GetNumber() - 1)
        end
    end)

    spinner:SetValue(minValue)
    return spinner
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.mkaSetScrollRange = 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.mkaGetScrollValue = 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.mkaSetScrollValue = 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.mkaOnScrollValueChanged then
            scrollFrame.mkaOnScrollValueChanged(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.mkaOnScrollValueChanged then
            frame.mkaOnScrollValueChanged(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.mkaSetScrollValue then
        scrollBar.mkaSetScrollValue(nextValue)
    elseif scrollBar and scrollBar.SetValue then
        scrollBar:SetValue(nextValue)
    end
end

local function SetPanelAccentGradient(texture, startAlpha, endAlpha)
    if texture.SetGradient and CreateColor then
        local ok = pcall(
            texture.SetGradient,
            texture,
            "VERTICAL",
            CreateColor(1, 0.82, 0, startAlpha),
            CreateColor(1, 0.82, 0, endAlpha)
        )
        if ok then
            return
        end
    end
    if texture.SetGradientAlpha then
        texture:SetGradientAlpha("VERTICAL", 1, 0.82, 0, startAlpha, 1, 0.82, 0, endAlpha)
    else
        texture:SetVertexColor(1, 0.82, 0, math.max(startAlpha or 0, endAlpha or 0))
    end
end

local function CreatePanelCenterAccent(parent)
    local accent = CreateFrame("Frame", nil, parent)
    accent:SetPoint("TOP", parent, "TOP", 0, 0)
    accent:SetPoint("BOTTOM", parent, "BOTTOM", 0, 0)
    accent:SetPoint("CENTER", parent, "CENTER", 0, 0)
    accent:SetWidth(1)

    local top = accent:CreateTexture(nil, "OVERLAY")
    top:SetTexture("Interface\\Buttons\\WHITE8X8")
    top:SetPoint("TOPLEFT", accent, "TOPLEFT", 0, 0)
    top:SetPoint("BOTTOMRIGHT", accent, "RIGHT", 0, 0)
    SetPanelAccentGradient(top, 1, 0)

    local bottom = accent:CreateTexture(nil, "OVERLAY")
    bottom:SetTexture("Interface\\Buttons\\WHITE8X8")
    bottom:SetPoint("TOPLEFT", accent, "LEFT", 0, 0)
    bottom:SetPoint("BOTTOMRIGHT", accent, "BOTTOMRIGHT", 0, 0)
    SetPanelAccentGradient(bottom, 0, 1)

    return accent
end

local function AddFindGroupPanelDivider(container, leftPanel, rightPanel)
    local divider = CreateFrame("Frame", nil, container)
    divider:SetPoint("TOP", leftPanel, "TOPRIGHT", PANEL_DIVIDER_OFFSET_X, -PANEL_DIVIDER_TOP_OFFSET)
    divider:SetPoint("BOTTOM", leftPanel, "BOTTOMRIGHT", PANEL_DIVIDER_OFFSET_X, PANEL_DIVIDER_BOTTOM_OFFSET)
    divider:SetWidth(PANEL_DIVIDER_WIDTH)
    divider:SetFrameLevel(math.max(leftPanel:GetFrameLevel(), rightPanel:GetFrameLevel()) + 2)

    local leftShadow = divider:CreateTexture(nil, "ARTWORK", nil, 1)
    leftShadow:SetPoint("TOPLEFT", divider, "TOPLEFT", 0, 0)
    leftShadow:SetPoint("BOTTOMLEFT", divider, "BOTTOMLEFT", 0, 0)
    leftShadow:SetWidth(2)
    leftShadow:SetTexture("Interface\\Buttons\\WHITE8X8")
    leftShadow:SetVertexColor(PANEL_DIVIDER_SHADOW_COLOR[1], PANEL_DIVIDER_SHADOW_COLOR[2], PANEL_DIVIDER_SHADOW_COLOR[3], PANEL_DIVIDER_SHADOW_COLOR[4])

    local center = divider:CreateTexture(nil, "ARTWORK", nil, 2)
    center:SetPoint("TOPLEFT", leftShadow, "TOPRIGHT", 0, 0)
    center:SetPoint("BOTTOMRIGHT", divider, "BOTTOMRIGHT", -2, 0)
    center:SetTexture("Interface\\Buttons\\WHITE8X8")
    center:SetVertexColor(PANEL_DIVIDER_COLOR[1], PANEL_DIVIDER_COLOR[2], PANEL_DIVIDER_COLOR[3], PANEL_DIVIDER_COLOR[4])

    local highlight = divider:CreateTexture(nil, "ARTWORK", nil, 3)
    highlight:SetPoint("TOPLEFT", center, "TOPLEFT", 0, 0)
    highlight:SetPoint("BOTTOMLEFT", center, "BOTTOMLEFT", 0, 0)
    highlight:SetWidth(1)
    highlight:SetTexture("Interface\\Buttons\\WHITE8X8")
    highlight:SetVertexColor(PANEL_DIVIDER_HIGHLIGHT_COLOR[1], PANEL_DIVIDER_HIGHLIGHT_COLOR[2], PANEL_DIVIDER_HIGHLIGHT_COLOR[3], PANEL_DIVIDER_HIGHLIGHT_COLOR[4])

    local rightShadow = divider:CreateTexture(nil, "ARTWORK", nil, 1)
    rightShadow:SetPoint("TOPRIGHT", divider, "TOPRIGHT", 0, 0)
    rightShadow:SetPoint("BOTTOMRIGHT", divider, "BOTTOMRIGHT", 0, 0)
    rightShadow:SetWidth(2)
    rightShadow:SetTexture("Interface\\Buttons\\WHITE8X8")
    rightShadow:SetVertexColor(PANEL_DIVIDER_SHADOW_COLOR[1], PANEL_DIVIDER_SHADOW_COLOR[2], PANEL_DIVIDER_SHADOW_COLOR[3], PANEL_DIVIDER_SHADOW_COLOR[4])

    divider.CenterAccent = CreatePanelCenterAccent(divider)
    container.MSPanelDivider = divider
    return divider
end

local function CreateCheckButton(parent, labelText, width, onClick)
    local button = CreateFrame("CheckButton", nil, parent, "UICheckButtonTemplate")
    button:SetSize(20, 20)
    button.Text:SetText(labelText or "")
    button.Text:ClearAllPoints()
    button.Text:SetPoint("LEFT", button, "RIGHT", 1, 0)
    button.Text:SetWidth(math.max(1, (width or 70) - 22))
    button.Text:SetJustifyH("LEFT")
    if core.ApplyFont then
        core.ApplyFont(button.Text, 12, "")
    end
    button:SetScript("OnClick", function(self)
        if onClick then
            onClick(self:GetChecked() == true)
        end
    end)
    button.mkaFindGroupWidth = width or 70
    return button
end

local function CreateDungeonDropdown(parent, owner)
    local dropdown = CreateFrame("DropdownButton", "MKAFindGroupDungeonDropdown", parent, "WowStyle1DropdownTemplate")
    dropdown:SetWidth(FORM_CONTROL_WIDTH)
    if dropdown.SetHeight then
        dropdown:SetHeight(24)
    end
    dropdown:SetSelectionText(function()
        return FindGroupSearch.GetSelectedDungeonText(owner)
    end)
    dropdown:SetupMenu(function(_, rootDescription)
        rootDescription:SetTag("MENU_MKA_FIND_GROUP_DUNGEONS")
        rootDescription:CreateButton(TRANSLATE("FIND_GROUP.DUNGEON.SELECT_ALL"), function()
            FindGroupSearch.SelectAllDungeons(owner)
            RefreshDropdown(dropdown)
            owner:RefreshMainWindow("FindGroupSettings")
        end)
        rootDescription:CreateButton(TRANSLATE("FIND_GROUP.DUNGEON.CLEAR"), function()
            FindGroupSearch.ClearDungeonSelection(owner)
            RefreshDropdown(dropdown)
            owner:RefreshMainWindow("FindGroupSettings")
        end)
        if rootDescription.CreateDivider then
            rootDescription:CreateDivider()
        end
        for _, option in ipairs(FindGroupSearch.GetDungeonOptions(owner)) do
            local text = option.activityName or tostring(option.activityID or "")
            local activityID = tonumber(option.activityID)
            if rootDescription.CreateCheckbox then
                rootDescription:CreateCheckbox(text, function(data)
                    return FindGroupSearch.IsDungeonSelected(owner, data)
                end, function(data)
                    FindGroupSearch.ToggleDungeon(owner, data)
                    RefreshDropdown(dropdown)
                    owner:RefreshMainWindow("FindGroupSettings")
                end, activityID)
            else
                rootDescription:CreateButton(text, function(data)
                    FindGroupSearch.ToggleDungeon(owner, data)
                    RefreshDropdown(dropdown)
                    owner:RefreshMainWindow("FindGroupSettings")
                end, activityID)
            end
        end
    end)
    return dropdown
end

local function SetRegionShown(region, shown)
    if not region then
        return
    end
    if shown then
        region:Show()
    else
        region:Hide()
    end
end

local function SetCheckButtonValue(button, checked)
    if button and button.SetChecked then
        button:SetChecked(checked == true)
    end
end

local function SetFindGroupButtonEnabled(button, enabled)
    if not button then
        return
    end
    if enabled then
        button:Enable()
    else
        button:Disable()
    end
    if button.SetAlpha then
        button:SetAlpha(enabled and 1 or 0.45)
    end
    if core.MeetingStoneButtonArt and core.MeetingStoneButtonArt.Update then
        core.MeetingStoneButtonArt.Update(button)
    end
end

local function SetFindGroupButtonText(button, text)
    if not button then
        return
    end
    if button.SetText then
        button:SetText(text or "")
    end
    local fontString = button.Text or (button.GetFontString and button:GetFontString())
    if fontString then
        fontString:SetText(text or "")
    end
end

local function SetEditBoxNumber(editBox, value)
    if not editBox then
        return
    end

    local numberValue = tonumber(value) or 0
    if editBox.mkaFindGroupMinValue or editBox.mkaFindGroupMaxValue then
        numberValue = math.max(
            tonumber(editBox.mkaFindGroupMinValue) or -math.huge,
            math.min(tonumber(editBox.mkaFindGroupMaxValue) or math.huge, math.floor(numberValue))
        )
    end

    editBox.mkaFindGroupProgrammaticSetValue = true
    if editBox.SetValue then
        editBox:SetValue(numberValue)
    elseif editBox.GetText and tostring(numberValue) ~= (editBox:GetText() or "") then
        editBox:SetText(tostring(numberValue))
    end
    editBox.mkaFindGroupProgrammaticSetValue = nil
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
    local offsetY = pressed and -1 or 0
    text:ClearAllPoints()
    text:SetPoint("LEFT", label, "LEFT", 2, offsetY)
    text:SetPoint("RIGHT", label, "RIGHT", -2, offsetY)
    text:SetPoint("CENTER", label, "CENTER", 0, offsetY)
    text:SetAlpha(pressed and 0.72 or 1)
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)
            if key == sortKey then
                label.text:SetTextColor(HEADER_SORTED_TEXT_COLOR[1], HEADER_SORTED_TEXT_COLOR[2], HEADER_SORTED_TEXT_COLOR[3], HEADER_SORTED_TEXT_COLOR[4])
            else
                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
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]), 13, "CENTER")
        label.text = text
        SetHeaderTextPressed(label, false)
        if SORTABLE_HEADER_KEYS[columnKey] then
            label:RegisterForClicks("LeftButtonUp")
            label:SetScript("OnMouseDown", function(self)
                SetHeaderTextPressed(self, true)
            end)
            label:SetScript("OnMouseUp", function(self)
                SetHeaderTextPressed(self, false)
            end)
            label:SetScript("OnLeave", function(self)
                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 ApplyHeaderLayout(header, availableWidth)
    local layout = GetColumnLayout(availableWidth)
    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
    header.mkaFindGroupColumnLayout = 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.mkaFindGroupAnimations = frame.mkaFindGroupAnimations or {}
    local group = frame.mkaFindGroupAnimations[key]
    if not group then
        group = frame:CreateAnimationGroup()
        local alpha = group:CreateAnimation("Alpha")
        alpha:SetOrder(1)
        group.mkaAlpha = alpha
        frame.mkaFindGroupAnimations[key] = group
    end
    group:Stop()
    group.mkaAlpha:SetFromAlpha(fromAlpha or 0)
    group.mkaAlpha:SetToAlpha(toAlpha or 1)
    group.mkaAlpha:SetDuration(duration or ROW_FADE_IN_SECONDS)
    if smoothing and group.mkaAlpha.SetSmoothing then
        group.mkaAlpha: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.mkaFindGroupAnimations and frame.mkaFindGroupAnimations[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.mkaFindGroupFadeSearchRequestID ~= requestID then
            widgets.mkaFindGroupFadeSearchRequestID = requestID
            widgets.mkaFindGroupFadedResultIDs = {}
        end
        widgets.mkaFindGroupFadedResultIDs = widgets.mkaFindGroupFadedResultIDs or {}
        widgets.mkaFindGroupFadedResultIDs[key] = true
        row.mkaFindGroupFadeKey = key
        return key, widgets.mkaFindGroupFadedResultIDs
    end
    row.mkaFindGroupFadeKey = key
    return key, nil
end

local function CompleteRowFadeIn(row, resultID, widgets, state, fadeToken)
    if not row then
        return
    end
    if fadeToken and row.mkaFindGroupFadeToken ~= 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.mkaFindGroupFadeToken = (tonumber(row.mkaFindGroupFadeToken) or 0) + 1
        StopAlphaAnimation(row, "appear", 1)
        return
    end

    if animate == false then
        row.mkaFindGroupFadeToken = (tonumber(row.mkaFindGroupFadeToken) 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.mkaFindGroupFadeSearchRequestID ~= requestID then
            widgets.mkaFindGroupFadeSearchRequestID = requestID
            widgets.mkaFindGroupFadedResultIDs = {}
        end
        widgets.mkaFindGroupFadedResultIDs = widgets.mkaFindGroupFadedResultIDs or {}
        fadedResults = widgets.mkaFindGroupFadedResultIDs
        if fadedResults[key] then
            row.mkaFindGroupFadeToken = (tonumber(row.mkaFindGroupFadeToken) or 0) + 1
            row.mkaFindGroupFadeKey = key
            StopAlphaAnimation(row, "appear", 1)
            return
        end
    elseif row.mkaFindGroupFadeKey == key then
        row.mkaFindGroupFadeToken = (tonumber(row.mkaFindGroupFadeToken) or 0) + 1
        StopAlphaAnimation(row, "appear", 1)
        return
    end

    row.mkaFindGroupFadeKey = key
    row.mkaFindGroupFadeToken = (tonumber(row.mkaFindGroupFadeToken) or 0) + 1
    local fadeToken = row.mkaFindGroupFadeToken
    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.mkaFindGroupRemovalResultID == resultID and row.mkaFindGroupRemovalToken == token then
        return
    end

    row.mkaFindGroupRemovalResultID = resultID
    row.mkaFindGroupRemovalToken = token
    StopAlphaAnimation(row, "appear", row.GetAlpha and row:GetAlpha() or 1)
    local function finishRemoval()
        if row.mkaFindGroupRemovalResultID ~= resultID or row.mkaFindGroupRemovalToken ~= token then
            return
        end
        if FindGroupSearch.CompleteReportRemoval then
            FindGroupSearch.CompleteReportRemoval(MKA, 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.mkaFindGroupBackgroundKey and row.mkaFindGroupBackgroundKey ~= key and widgets.backgroundTransition then
        widgets.backgroundTransition:SetTexture(row.mkaFindGroupBackgroundTexture or ROW_TEXTURE_NORMAL)
        widgets.backgroundTransition:SetVertexColor(
            row.mkaFindGroupBackgroundColorR or 1,
            row.mkaFindGroupBackgroundColorG or 1,
            row.mkaFindGroupBackgroundColorB or 1,
            row.mkaFindGroupBackgroundColorA 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.mkaFindGroupBackgroundKey = key
    row.mkaFindGroupBackgroundTexture = texturePath or ROW_TEXTURE_NORMAL
    row.mkaFindGroupBackgroundColorR = color[1] or 1
    row.mkaFindGroupBackgroundColorG = color[2] or 1
    row.mkaFindGroupBackgroundColorB = color[3] or 1
    row.mkaFindGroupBackgroundColorA = 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 MKA and MKA.SetGroupRowHover then
        MKA: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 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.mkaFindGroupCancelIcon
    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.mkaFindGroupCancelIcon = 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("FIND_GROUP.APPLICATION.CANCEL"))
            GameTooltip:Show()
        end
    end)
    button:SetScript("OnLeave", function(self)
        self.mkaFindGroupButtonPressed = 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.mkaFindGroupButtonPressed = 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.mkaFindGroupButtonPressed = 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(MKA, resultID)
        if not ok and MKA and MKA.Print then
            MKA:Print(reason)
        end
        if MKA and MKA.RefreshMainWindow then
            MKA: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.mkaFindGroupColumnLayout = 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.mkaFindGroupColumnLayout
    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("LEFT")
        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 totalWidth = APPLICATION_STATUS_ICON_SIZE + APPLICATION_STATUS_ICON_GAP + textWidth
    local groupLeft = column.x + 2 + math.max(0, math.floor((availableWidth - totalWidth) / 2))
    if statusFrame then
        statusFrame:ClearAllPoints()
        statusFrame:SetPoint("LEFT", row, "LEFT", groupLeft, 0)
        if applicationState == "pending" then
            StartFindGroupPendingSpinner(statusFrame)
        else
            statusFrame:Show()
        end
        widgets.comment:SetPoint("LEFT", statusFrame, "RIGHT", APPLICATION_STATUS_ICON_GAP, 0)
    else
        widgets.comment:SetPoint("LEFT", row, "LEFT", groupLeft, 0)
    end
    widgets.comment:SetSize(math.max(1, availableWidth - math.max(0, groupLeft - (column.x + 2)) - APPLICATION_STATUS_ICON_SIZE - APPLICATION_STATUS_ICON_GAP), 18)
    widgets.comment:SetJustifyH("LEFT")
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)
    local textWidth = math.ceil(widgets.typeText:GetStringWidth() or 0)
    local gap = hasIcon and textWidth > 0 and 2 or 0
    local totalWidth = (hasIcon and TYPE_ICON_SIZE or 0) + gap + textWidth
    local left = math.max(0, math.floor((columnWidth - totalWidth) / 2))

    widgets.typeIcon:ClearAllPoints()
    widgets.typeText:ClearAllPoints()
    widgets.typeText:SetJustifyH("LEFT")

    if hasIcon then
        widgets.typeIcon:SetSize(TYPE_ICON_SIZE, TYPE_ICON_SIZE)
        widgets.typeIcon:SetPoint("LEFT", widgets.typeContainer, "LEFT", left, 0)
        widgets.typeText:SetPoint("LEFT", widgets.typeIcon, "RIGHT", gap, 0)
        widgets.typeText:SetSize(math.max(TYPE_TEXT_MIN_WIDTH, columnWidth - left - TYPE_ICON_SIZE - gap), 18)
        widgets.typeIcon:Show()
    else
        widgets.typeIcon:Hide()
        widgets.typeText:SetPoint("LEFT", widgets.typeContainer, "LEFT", left, 0)
        widgets.typeText:SetSize(math.max(TYPE_TEXT_MIN_WIDTH, textWidth + TYPE_TEXT_PADDING), 18)
    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 == "bnet" or resultType == "guild" then
        return { 0.35, 0.75, 1, 0.16 }
    end
    if resultType == "friend" then
        return { 0.2, 1, 0.2, 0.14 }
    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("FIND_GROUP.TYPE.LEAVER"))
        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("FIND_GROUP.TYPE.NEWBIE"))
        widgets.typeText:SetTextColor(0.12, 1, 0.25, 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("FIND_GROUP.TYPE.BNET"))
        widgets.typeText:SetTextColor(0.35, 0.75, 1, 1)
        hasIcon = true
    elseif resultType == "friend" then
        widgets.typeText:SetText(TRANSLATE("FIND_GROUP.TYPE.FRIEND"))
        widgets.typeText:SetTextColor(0.2, 1, 0.2, 1)
    elseif resultType == "guild" then
        widgets.typeText:SetText(TRANSLATE("FIND_GROUP.TYPE.GUILD"))
        widgets.typeText:SetTextColor(0.35, 0.75, 1, 1)
    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.mkaFindGroupResult = 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("FIND_GROUP.APPLICATION.PENDING_FORMAT"), 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("FIND_GROUP.APPLICATION.DECLINED"))
        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("FIND_GROUP.APPLICATION.CANCELLED"))
        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.mkaFindGroupRemovalResultID = nil
        row.mkaFindGroupRemovalToken = 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.mkaGetScrollValue then
        currentValue = scrollBar.mkaGetScrollValue() or 0
    elseif scrollBar.GetValue then
        currentValue = scrollBar:GetValue() or 0
    end
    currentValue = math.min(math.max(currentValue, 0), maxScroll)

    if scrollBar.mkaSetScrollRange then
        scrollBar.mkaSetScrollRange(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.mkaGetScrollValue then
        return scrollBar.mkaGetScrollValue() 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
    results = results or state and state.results or {}
    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.mkaFindGroupResult = 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.mkaFindGroupFadeKey = nil
            row.mkaFindGroupRemovalResultID = nil
            row.mkaFindGroupRemovalToken = nil
        end
    end
    UpdateFindGroupScrollState(widgets, totalRows)
    QueueVisibleResultDetails(owner, widgets, results)

    if totalRows > 0 then
        widgets.findGroupEmpty:Hide()
        SetFindGroupLoadingAnimationShown(widgets, false, false)
    else
        local emptyText = state.lastError
            or isLoading and TRANSLATE("FIND_GROUP.EMPTY.LOADING_LIST")
            or state.searchCompletedEmpty == true and TRANSLATE("FIND_GROUP.EMPTY.NO_MATCHES")
            or TRANSLATE("FIND_GROUP.EMPTY.NO_RESULTS")
        widgets.findGroupEmpty:SetTextColor(EMPTY_TEXT_COLOR[1], EMPTY_TEXT_COLOR[2], EMPTY_TEXT_COLOR[3], EMPTY_TEXT_COLOR[4])
        widgets.findGroupEmpty:SetText(emptyText)
        UpdateFindGroupEmptyLayout(widgets, isLoading)
        widgets.findGroupEmpty:Show()
        SetFindGroupLoadingAnimationShown(widgets, isLoading, isLoading)
    end
end

local function RefreshControls(owner, widgets)
    local settings = FindGroupSearch.GetSettings(owner) or {}
    if widgets.findGroupDungeonDropdown then
        RefreshDropdown(widgets.findGroupDungeonDropdown)
    end
    SetCheckButtonValue(widgets.findGroupHaveHealerCheck, settings.haveHealer)
    SetCheckButtonValue(widgets.findGroupNoHealerCheck, settings.noHealer)
    SetCheckButtonValue(widgets.findGroupHaveTankCheck, settings.haveTank)
    SetCheckButtonValue(widgets.findGroupNoTankCheck, settings.noTank)
    SetCheckButtonValue(widgets.findGroupNoDpsCheck, settings.noDps)
    SetCheckButtonValue(widgets.findGroupSearchNewbieCheck, settings.searchNewbie)
    SetCheckButtonValue(widgets.findGroupQuickJoinCheck, settings.quickJoin)
    SetCheckButtonValue(widgets.findGroupAutoJoinCheck, settings.autoJoin)
    SetEditBoxNumber(widgets.findGroupMinSlotsBox, settings.minAvailableSlots or 1)
    SetEditBoxNumber(widgets.findGroupMinScoreBox, settings.minScore or 0)
    local state = FindGroupSearch.GetState and FindGroupSearch.GetState(owner) or {}
    local hasSelectedDungeons = FindGroupSearch.HasSelectedDungeons and FindGroupSearch.HasSelectedDungeons(owner)
    local hasSearch = state.searching == true
        or state.processing == true
        or state.newbieSearching == true
        or #(state.results or {}) > 0
    SetFindGroupButtonText(widgets.findGroupSearchButton, TRANSLATE(hasSearch and "FIND_GROUP.ACTION.REFRESH" or "FIND_GROUP.ACTION.SEARCH"))
    SetFindGroupButtonEnabled(widgets.findGroupSearchButton, hasSelectedDungeons == true)
end

local function AddLeftHeader(panel)
    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)

    local title = panel:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    title:SetPoint("TOPLEFT", background, "TOPLEFT", HEADER_CONTENT_INSET_X, HEADER_CONTENT_OFFSET_Y)
    title:SetPoint("BOTTOMRIGHT", background, "BOTTOMRIGHT", -HEADER_CONTENT_INSET_X, HEADER_CONTENT_OFFSET_Y)
    title:SetJustifyH("CENTER")
    title:SetJustifyV("MIDDLE")
    title:SetText(TRANSLATE("TAB.FIND_GROUP"))
    title:SetTextColor(1, 0.82, 0, 1)
    if core.ApplyFont then
        core.ApplyFont(title, HEADER_TEXT_SIZE, "OUTLINE")
    end
    panel.HeaderBackground = background
    panel.HeaderTitle = title
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 CreateLeftPanel(owner, container)
    local panel = CreateFrame("Frame", nil, container, "BackdropTemplate")
    panel:SetPoint("TOPLEFT", container, "TOPLEFT", 0, 0)
    panel:SetPoint("BOTTOMLEFT", container, "BOTTOMLEFT", 0, 0)
    panel:SetWidth(LEFT_PANEL_WIDTH)

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

    AddLeftHeader(panel)

    local y = FORM_TOP_OFFSET
    local function addTitle(text)
        local label = CreateFindGroupLabel(panel, text)
        label:SetPoint("TOPLEFT", panel, "TOPLEFT", FORM_INSET_X, y)
        label:SetWidth(FORM_CONTROL_WIDTH)
        label:SetHeight(16)
        y = y - 16 - FORM_LABEL_TO_CONTROL_GAP
        return label
    end
    local function addControl(control, height)
        control:SetPoint("TOPLEFT", panel, "TOPLEFT", FORM_INSET_X, y)
        y = y - (height or FORM_CONTROL_HEIGHT) - FORM_ROW_GAP
        return control
    end
    local function addCheck(text, onClick)
        local check = CreateCheckButton(panel, text, FORM_CONTROL_WIDTH, onClick)
        check:SetPoint("TOPLEFT", panel, "TOPLEFT", FORM_INSET_X, y)
        y = y - FORM_CONTROL_HEIGHT
        return check
    end
    local function addTwoChecks(leftText, rightText, onLeft, onRight)
        local columnGap = 4
        local columnWidth = math.floor((FORM_CONTROL_WIDTH - columnGap) / 2)
        local leftCheck = CreateCheckButton(panel, leftText, columnWidth, onLeft)
        leftCheck:SetPoint("TOPLEFT", panel, "TOPLEFT", FORM_INSET_X, y)
        local rightCheck
        if rightText then
            rightCheck = CreateCheckButton(panel, rightText, columnWidth, onRight)
            rightCheck:SetPoint("TOPLEFT", panel, "TOPLEFT", FORM_INSET_X + columnWidth + columnGap, y)
        end
        y = y - FORM_CONTROL_HEIGHT
        return leftCheck, rightCheck
    end
    local function addNumberInput(text, defaultValue, onChanged, options)
        local label = CreateFindGroupLabel(panel, text)
        label:SetPoint("LEFT", panel, "TOPLEFT", FORM_INSET_X, y - (FORM_CONTROL_HEIGHT / 2))
        label:SetWidth(FORM_INLINE_LABEL_WIDTH)
        label:SetHeight(FORM_CONTROL_HEIGHT)

        local minValue = options and options.minValue
        local maxValue = options and options.maxValue
        local box = options and options.spinner
            and CreateFindGroupNumericSpinner(panel, minValue, maxValue)
            or CreateFindGroupInput(panel, FORM_INLINE_INPUT_WIDTH, FORM_CONTROL_HEIGHT, true)
        if not box.mkaFindGroupUsesNativeSpinner then
            box.mkaFindGroupMinValue = minValue
            box.mkaFindGroupMaxValue = maxValue
        end

        if box.mkaFindGroupUsesNativeSpinner then
            box:SetPoint("TOPRIGHT", panel, "TOPLEFT", FORM_INSET_X + FORM_CONTROL_WIDTH + 8, y)
        else
            box:SetPoint("TOPRIGHT", panel, "TOPLEFT", FORM_INSET_X + FORM_CONTROL_WIDTH, y)
        end

        SetEditBoxNumber(box, defaultValue or 0)
        EnableSelectAllAfterMouseLeave(box.mkaFindGroupEditBox or box)
        local function handleValueChanged(input, value)
            if input and input.mkaFindGroupProgrammaticSetValue then
                return
            end
            if onChanged then
                onChanged(tonumber(value) or input and input.GetNumber and input:GetNumber() or 0)
            end
        end
        if box.SetOnValueChangedCallback then
            box:SetOnValueChangedCallback(handleValueChanged)
        else
            box:SetScript("OnTextChanged", function(self)
                handleValueChanged(self, self:GetNumber())
            end)
        end
        y = y - FORM_CONTROL_HEIGHT - FORM_ROW_GAP
        return label, box
    end
    local dungeonLabel = addTitle(TRANSLATE("MEETINGSTONE.FORM.DUNGEON"))
    local dungeonDropdown = addControl(CreateDungeonDropdown(panel, owner), FORM_CONTROL_HEIGHT)

    local roleLabel = addTitle(TRANSLATE("FIND_GROUP.FILTER.ROLES"))
    local haveHealerCheck, noHealerCheck = addTwoChecks(
        TRANSLATE("FIND_GROUP.FILTER.HAVE_HEALER"),
        TRANSLATE("FIND_GROUP.FILTER.NO_HEALER"),
        function(value)
            FindGroupSearch.SetBooleanFilter(owner, "haveHealer", value)
            owner:RefreshMainWindow("FindGroupSettings")
        end,
        function(value)
            FindGroupSearch.SetBooleanFilter(owner, "noHealer", value)
            owner:RefreshMainWindow("FindGroupSettings")
        end
    )
    local haveTankCheck, noTankCheck = addTwoChecks(
        TRANSLATE("FIND_GROUP.FILTER.HAVE_TANK"),
        TRANSLATE("FIND_GROUP.FILTER.NO_TANK"),
        function(value)
            FindGroupSearch.SetBooleanFilter(owner, "haveTank", value)
            owner:RefreshMainWindow("FindGroupSettings")
        end,
        function(value)
            FindGroupSearch.SetBooleanFilter(owner, "noTank", value)
            owner:RefreshMainWindow("FindGroupSettings")
        end
    )
    local noDpsCheck = addTwoChecks(TRANSLATE("FIND_GROUP.FILTER.NO_DPS"), nil, function(value)
        FindGroupSearch.SetBooleanFilter(owner, "noDps", value)
        owner:RefreshMainWindow("FindGroupSettings")
    end)

    y = y - 4
    local slotsLabel, slotsBox = addNumberInput(TRANSLATE("FIND_GROUP.FILTER.MIN_SLOTS"), 1, function(value)
        FindGroupSearch.SetNumericFilter(owner, "minAvailableSlots", value)
        owner:RefreshMainWindow("FindGroupSettings")
    end, {
        spinner = true,
        minValue = FIND_GROUP_MIN_AVAILABLE_SLOTS,
        maxValue = FIND_GROUP_MAX_AVAILABLE_SLOTS,
    })

    local scoreLabel, scoreBox = addNumberInput(TRANSLATE("FIND_GROUP.FILTER.MIN_SCORE"), 0, function(value)
        FindGroupSearch.SetNumericFilter(owner, "minScore", value)
        owner:RefreshMainWindow("FindGroupSettings")
    end)

    local searchNewbieCheck = addCheck(TRANSLATE("FIND_GROUP.FILTER.SEARCH_NEWBIE"), function(value)
        FindGroupSearch.SetBooleanFilter(owner, "searchNewbie", value)
        owner:RefreshMainWindow("FindGroupSettings")
    end)
    if searchNewbieCheck.Text and core.ApplyFont then
        core.ApplyFont(searchNewbieCheck.Text, 14, "")
    end

    y = y - 4
    local quickLabel = addTitle(TRANSLATE("FIND_GROUP.QUICK.TITLE"))
    local quickJoinCheck, autoJoinCheck = addTwoChecks(
        TRANSLATE("FIND_GROUP.QUICK.DOUBLE_CLICK"),
        TRANSLATE("FIND_GROUP.QUICK.AUTO_JOIN"),
        function(value)
            FindGroupSearch.SetBooleanFilter(owner, "quickJoin", value)
            owner:RefreshMainWindow("FindGroupSettings")
        end,
        function(value)
            FindGroupSearch.SetBooleanFilter(owner, "autoJoin", value)
            FindGroupSearch.EnsureAutoJoinHooks(owner)
            owner:RefreshMainWindow("FindGroupSettings")
        end
    )

    local leftBottomGradient = panel:CreateTexture(nil, "OVERLAY")
    leftBottomGradient:SetPoint("TOPLEFT", panel, "BOTTOMLEFT", 0, LEFT_BOTTOM_GRADIENT_TOP_OFFSET)
    leftBottomGradient:SetPoint("BOTTOMRIGHT", panel, "BOTTOMRIGHT", 0, 0)
    if TrySetAtlas(leftBottomGradient, "QuestLog-Frame-Gradient-bottom", false) then
        leftBottomGradient:SetAlpha(0.75)
    else
        leftBottomGradient:Hide()
    end
    panel.BottomGradient = leftBottomGradient

    local searchButton = CreateSystemButton(panel, nil, FORM_BUTTON_WIDTH, FORM_BUTTON_HEIGHT, TRANSLATE("FIND_GROUP.ACTION.SEARCH"), ACTION_BUTTON_TEXT_OFFSET_Y)
    searchButton:SetPoint("BOTTOMLEFT", panel, "BOTTOMLEFT", FORM_INSET_X, FORM_BUTTON_BOTTOM_OFFSET)
    searchButton:SetScript("OnClick", function()
        local ok, reason = FindGroupSearch.Search(owner)
        if not ok and owner.Print then
            owner:Print(reason)
        end
        owner:RefreshMainWindow("FindGroupSearch")
    end)
    local resetButton = CreateSystemButton(panel, nil, FORM_BUTTON_WIDTH, FORM_BUTTON_HEIGHT, TRANSLATE("FIND_GROUP.ACTION.RESET"), ACTION_BUTTON_TEXT_OFFSET_Y)
    resetButton:SetPoint("LEFT", searchButton, "RIGHT", FORM_BUTTON_GAP, 0)
    resetButton:SetScript("OnClick", function()
        FindGroupSearch.Reset(owner)
        owner:RefreshMainWindow("FindGroupReset")
    end)

    return panel, {
        findGroupDungeonLabel = dungeonLabel,
        findGroupDungeonDropdown = dungeonDropdown,
        findGroupRoleLabel = roleLabel,
        findGroupHaveHealerCheck = haveHealerCheck,
        findGroupNoHealerCheck = noHealerCheck,
        findGroupHaveTankCheck = haveTankCheck,
        findGroupNoTankCheck = noTankCheck,
        findGroupNoDpsCheck = noDpsCheck,
        findGroupMinSlotsLabel = slotsLabel,
        findGroupMinSlotsBox = slotsBox,
        findGroupMinScoreLabel = scoreLabel,
        findGroupMinScoreBox = scoreBox,
        findGroupSearchNewbieCheck = searchNewbieCheck,
        findGroupQuickLabel = quickLabel,
        findGroupQuickJoinCheck = quickJoinCheck,
        findGroupAutoJoinCheck = autoJoinCheck,
        findGroupSearchButton = searchButton,
        findGroupResetButton = resetButton,
    }
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)
    if scrollFrame.SetClipsChildren then
        scrollFrame:SetClipsChildren(true)
    end
    scrollFrame.mkaOnScrollValueChanged = 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("FIND_GROUP.EMPTY.NO_RESULTS"))
    if core.ApplyFont then
        core.ApplyFont(empty, 14, "")
    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,
        findGroupEmpty = empty,
        findGroupRows = {},
    }
end

function FindGroupTab.HandleRowClick(row)
    local owner = MKA
    local result = row and row.mkaFindGroupResult
    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.mkaFindGroupLastClickedResultID == result.resultID and now - (owner.mkaFindGroupLastClickAt or 0) <= 0.35 then
        if FindGroupSearch.CanApplyToResult and not FindGroupSearch.CanApplyToResult(owner, result.resultID) then
            owner.mkaFindGroupLastClickedResultID = nil
            owner.mkaFindGroupLastClickAt = 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.mkaFindGroupLastClickedResultID = result.resultID
    owner.mkaFindGroupLastClickAt = now
end

function FindGroupTab.ShowContextMenu(row)
    local owner = MKA
    local result = row and row.mkaFindGroupResult
    if not (owner and result and MenuUtil) then
        return
    end
    MenuUtil.CreateContextMenu(row, function(_, rootDescription)
        rootDescription:SetTag("MENU_MKA_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("FIND_GROUP.MENU.APPLY") .. "|r")
            or TRANSLATE("FIND_GROUP.MENU.APPLY")
        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("FIND_GROUP.MENU.WHISPER"), 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("FIND_GROUP.MENU.REPORT_GROUP"), function()
            local ok = FindGroupSearch.ReportGroup and FindGroupSearch.ReportGroup(owner, result)
            if not ok and owner.Print then
                owner:Print(TRANSLATE("FIND_GROUP.ERROR.REPORT_UNAVAILABLE"))
            end
        end)
        rootDescription:CreateButton(TRANSLATE("FIND_GROUP.MENU.REPORT_AD"), function()
            local ok = FindGroupSearch.ReportAdvertisement and FindGroupSearch.ReportAdvertisement(owner, result)
            if not ok and owner.Print then
                owner:Print(TRANSLATE("FIND_GROUP.ERROR.REPORT_UNAVAILABLE"))
            end
        end)
        rootDescription:CreateButton(TRANSLATE("FIND_GROUP.MENU.BLOCK_TITLE"), function()
            FindGroupSearch.BlockTitle(owner, result.title)
            owner:RefreshMainWindow("FindGroupBlockTitle")
        end)
    end)
end

function FindGroupTab.ShowTooltip(row)
    local result = row and row.mkaFindGroupResult
    if not result then
        return
    end
    if FindGroupSearch.EnsureResultDetails then
        result = FindGroupSearch.EnsureResultDetails(MKA, result.resultID) or result
        row.mkaFindGroupResult = 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_CURSOR")
    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("FIND_GROUP.TOOLTIP.LEADER"), ColorText(result.leaderDisplayName or "-", GetClassColor(result.leaderClassFile)), 1, 0.82, 0, 1, 1, 1)
    tooltip:AddDoubleLine(TRANSLATE("FIND_GROUP.TOOLTIP.SCORE"), ColorText(math.floor((tonumber(result.score) or 0) + 0.5), GetScoreColor(result.score)), 1, 0.82, 0, 1, 1, 1)
    tooltip:AddDoubleLine(TRANSLATE("FIND_GROUP.TOOLTIP.MEMBERS"), 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("FIND_GROUP.TOOLTIP.SLOTS"), tostring(result.availableSlots or 0), 1, 0.82, 0, 1, 1, 1)
    if tonumber(result.requiredItemLevel) and result.requiredItemLevel > 0 then
        tooltip:AddDoubleLine(TRANSLATE("FIND_GROUP.TOOLTIP.ITEM_LEVEL"), tostring(math.floor(result.requiredItemLevel + 0.5)), 1, 0.82, 0, 1, 1, 1)
    end
    if tonumber(result.age) then
        tooltip:AddDoubleLine(TRANSLATE("FIND_GROUP.TOOLTIP.AGE"), string.format(TRANSLATE("FIND_GROUP.TOOLTIP.AGE_FORMAT"), math.max(0, math.floor(result.age / 60))), 1, 0.82, 0, 1, 1, 1)
    end

    local newbieNames = {}
    local leaverNames = {}
    if #result.members > 0 then
        tooltip:AddLine(" ")
        tooltip:AddLine(TRANSLATE("FIND_GROUP.TOOLTIP.MEMBER_TITLE"), 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("FIND_GROUP.ROLE." .. tostring(roleKey or "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
            end
        end
        if shownMembers <= 0 then
            tooltip:AddLine(TRANSLATE("FIND_GROUP.TOOLTIP.MEMBERS_LOADING"), 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("FIND_GROUP.TOOLTIP.MEMBERS_LOADING"), 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("FIND_GROUP.TYPE.LEAVER"),
            #leaverNames > 0 and leaverNames or { TRANSLATE("FIND_GROUP.TOOLTIP.MEMBERS_LOADING") },
            1,
            0.08,
            0.05
        )
    end
    if result.hasNewbie == true or resultType == "newbie" then
        if not hasTypeWarning then
            tooltip:AddLine(" ")
        end
        AddTypeMemberTooltipLine(
            tooltip,
            NEWBIE_ICON_TEXTURE,
            TRANSLATE("FIND_GROUP.TYPE.NEWBIE"),
            #newbieNames > 0 and newbieNames or { TRANSLATE("FIND_GROUP.TOOLTIP.MEMBERS_LOADING") },
            0.12,
            1,
            0.25
        )
    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 FindGroupTab.Refresh(owner, widgets)
    if not widgets then
        return
    end
    FindGroupSearch.EnsureEventFrame(owner)
    RefreshControls(owner, widgets)
    if widgets.findGroupHeader and widgets.findGroupScrollFrame then
        RefreshHeaderSortState(widgets.findGroupHeader, owner)
        ApplyHeaderLayout(widgets.findGroupHeader, math.max(widgets.findGroupScrollFrame:GetWidth() - 12, 1))
    end
    RenderRows(owner, widgets)
end

function FindGroupTab.BuildSnapshot(owner)
    if FindGroupSearch.BuildSnapshot then
        return FindGroupSearch.BuildSnapshot(owner)
    end
    return ""
end

function FindGroupTab.Build(owner, tabGroup)
    FindGroupSearch.EnsureEventFrame(owner)

    local container = CreateFrame("Frame", nil, UIParent)
    container.mkaFillDecorFrame = true
    container.mkaDecorFrameInsetLeft = PANEL_INSET_LEFT
    container.mkaDecorFrameInsetTop = PANEL_INSET_TOP
    container.mkaDecorFrameInsetRight = PANEL_INSET_RIGHT
    container.mkaDecorFrameInsetBottom = PANEL_INSET_BOTTOM
    tabGroup:AddChild(container)

    local leftPanel, leftWidgets = CreateLeftPanel(owner, container)
    local rightPanel, rightWidgets = CreateRightPanel(owner, container, leftPanel)
    AddFindGroupPanelDivider(container, leftPanel, rightPanel)

    local widgets = {
        tabGroup = owner.widgets and owner.widgets.tabGroup or tabGroup,
        root = container,
        findGroupLeftPanel = leftPanel,
        findGroupRightPanel = rightPanel,
        skipApplyFontToFrame = true,
    }
    for key, value in pairs(leftWidgets) do
        widgets[key] = value
    end
    for key, value in pairs(rightWidgets) do
        widgets[key] = value
    end

    owner.widgets = widgets
    owner.tabViews = owner.tabViews or {}
    owner.tabViews.findgroup = widgets
    if core.ApplyThemeToTabGroup then
        core.ApplyThemeToTabGroup(tabGroup)
    end
    container:Show()
    FindGroupTab.Refresh(owner, widgets)
end
