local _, GF = ...

GF.FRAME_W_LEGACY_DEFAULT = 1024
-- 840px browse columns + 4px ColumnDisplay compensation + current panel/nav/scroll insets.
GF.FRAME_W = 1120
GF.FRAME_H = 540
GF.FRAME_MIN_W = GF.FRAME_W
GF.FRAME_MIN_H = GF.FRAME_H
GF.PANEL_SCALE_MIN_PCT = 100
GF.PANEL_SCALE_MAX_PCT = 150
GF.PANEL_SCALE_DEFAULT_PCT = 100
GF.FONT_SCALE_MIN_PCT = 100
GF.FONT_SCALE_MAX_PCT = 150
GF.FONT_SCALE_DEFAULT_PCT = 100
GF.LIST_BACKGROUND_ALPHA_MIN_PCT = 30
GF.LIST_BACKGROUND_ALPHA_MAX_PCT = 100
GF.LIST_BACKGROUND_ALPHA_DEFAULT_PCT = 100
GF.DEFAULT_REQUIRED_ITEM_LEVEL_MIN = 0
GF.DEFAULT_REQUIRED_ITEM_LEVEL_DEFAULT = 0
GF.FRAME_PAD = 4
GF.FRAME_TITLE_TOP = 24
GF.FRAME_BODY_BOTTOM = 24
GF.FRAME_CONTENT_BOTTOM = GF.FRAME_BODY_BOTTOM + GF.FRAME_PAD
GF.MAIN_PANEL_INSET_LEFT = 26
GF.MAIN_PANEL_INSET_RIGHT = 26
GF.MAIN_PANEL_INSET_TOP = 64
GF.MAIN_PANEL_INSET_BOTTOM = 24
GF.MAIN_PANEL_BACKPLATE_BG_INSET_LEFT = 5
GF.MAIN_PANEL_BACKPLATE_BG_INSET_RIGHT = 5
GF.MAIN_PANEL_BACKPLATE_BG_INSET_TOP = 2
GF.MAIN_PANEL_BACKPLATE_BG_INSET_BOTTOM = 10
GF.MAIN_PANEL_BACKPLATE_BG_COLOR = { 0, 0, 0, 1 }
GF.MAIN_PANEL_CONTENT_PADDING_X = GF.MAIN_PANEL_BACKPLATE_BG_INSET_LEFT
GF.MAIN_PANEL_CONTENT_PADDING_TOP = GF.MAIN_PANEL_BACKPLATE_BG_INSET_TOP
GF.MAIN_PANEL_CONTENT_PADDING_BOTTOM = GF.MAIN_PANEL_BACKPLATE_BG_INSET_BOTTOM
GF.MAIN_PANEL_TAB_MIN_WIDTH = 95
GF.MAIN_PANEL_TAB_MAX_WIDTH = 170
GF.MAIN_PANEL_TAB_HEIGHT = 32
GF.MAIN_PANEL_TAB_GAP = 1
GF.MAIN_WINDOW_TITLE_OFFSET_Y = -6
GF.MAIN_WINDOW_DRAG_HANDLE_LEFT_INSET = 18
GF.MAIN_WINDOW_DRAG_HANDLE_RIGHT_INSET = 46
GF.MAIN_WINDOW_DRAG_HANDLE_TOP_OFFSET = 0
GF.MAIN_WINDOW_DRAG_HANDLE_BOTTOM_OFFSET = -40
GF.MAIN_WINDOW_ROLE_BUTTON_SIZE = 32
GF.MAIN_WINDOW_ROLE_BUTTON_GAP = 9
GF.MAIN_WINDOW_ROLE_BUTTON_RIGHT_INSET = 48
GF.MAIN_WINDOW_ROLE_BUTTON_CENTER_OFFSET = 46
GF.MAIN_WINDOW_ROLE_BUTTON_TOP_OFFSET = GF.MAIN_WINDOW_ROLE_BUTTON_CENTER_OFFSET - (GF.MAIN_WINDOW_ROLE_BUTTON_SIZE / 2)
GF.MAIN_WINDOW_EYE_SIZE = 44
GF.MAIN_WINDOW_EYE_HOST_SIZE = 68
GF.MAIN_WINDOW_EYE_BACKGROUND_SIZE = 54
GF.MAIN_WINDOW_EYE_BACKGROUND_MASK = "Interface\\CharacterFrame\\TempPortraitAlphaMask"
GF.ADDON_LOGO_TEXTURE = "Interface\\AddOns\\GroupFinder\\Art\\Logo\\GroupFinderIcon.png"
GF.ADDON_MENU_LOGO_TEXTURE = "Interface\\AddOns\\GroupFinder\\Art\\Logo\\GroupFinder.png"
GF.ADDON_ART_UI_PATH = "Interface\\AddOns\\GroupFinder\\Art\\UI\\"
GF.ADDON_SOUNDS_PATH = "Interface\\AddOns\\GroupFinder\\Sounds\\"
GF.COMMON_BUTTON_TEXTURE = GF.ADDON_ART_UI_PATH .. "RedButton.png"
GF.FILTER_CHECK_ATLAS_TEXTURE = GF.ADDON_ART_UI_PATH .. "FilterCheckAtlas.png"
GF.REFRESH_TEXTURE = GF.ADDON_ART_UI_PATH .. "Refresh.png"
GF.TEAMUP_TEXTURE = GF.ADDON_ART_UI_PATH .. "TeamUp.png"
GF.FLOATING_TEXTURE = GF.ADDON_ART_UI_PATH .. "Floating.png"
GF.JOIN_ANNOUNCE_TOAST_ATLAS = "evergreen-scenario-TitleBG"
GF.JOIN_ANNOUNCE_TOAST_SOUND = GF.ADDON_SOUNDS_PATH .. "Glass.aiff"
GF.ROLE_VACANCY_THRESHOLD_MIN = 1
GF.ROLE_VACANCY_THRESHOLD_MAX = 4
GF.CHAT_RESET_COLOR_CODE = "|r"
GF.CHAT_WARNING_PREFIX_COLOR_CODE = "|cffffd200"
GF.CHAT_WARNING_BODY_COLOR_CODE = "|cffff80ff"
GF.CHAT_STATUS_PREFIX_COLOR_CODE = "|cff00ff00"
GF.CHAT_STATUS_BODY_COLOR_CODE = "|cffffffff"
GF.CHAT_STATUS_HIGHLIGHT_COLOR_CODE = "|cffffd200"
GF.CHAT_STATUS_CONFIRM_COLOR_CODE = "|cff00ff00"
GF.CHAT_STATUS_FAILURE_COLOR_CODE = "|cffff4040"
GF.WARNING_PREFIX_COLOR_CODE = GF.CHAT_WARNING_PREFIX_COLOR_CODE
GF.WARNING_RESET_COLOR_CODE = GF.CHAT_RESET_COLOR_CODE
GF.WARNING_COLOR = { 1, 0.5, 1, 1 }
GF.STATUS_MESSAGE_COLOR = { 1, 1, 1, 1 }

local ADDON_CHAT_PREFIXES = {
	"队伍查找器：",
	"GroupFinder:",
}

local function getAddonChatPrefix()
	local addonName = (GF.L and GF.L.ADDON_NAME) or "GroupFinder"
	return addonName == "队伍查找器" and (addonName .. "：") or (addonName .. ":")
end

local function splitAddonChatMessage(msg)
	msg = tostring(msg or "")
	for _, prefix in ipairs(ADDON_CHAT_PREFIXES) do
		if msg:sub(1, #prefix) == prefix then
			return prefix, msg:sub(#prefix + 1)
		end
	end
	local prefix = getAddonChatPrefix()
	local separator = prefix == "GroupFinder:" and " " or ""
	return prefix, separator .. msg
end

local function colorWrap(colorCode, text)
	return (colorCode or "") .. tostring(text or "") .. (GF.CHAT_RESET_COLOR_CODE or "|r")
end

local function escapePattern(text)
	return (tostring(text or ""):gsub("([%(%)%.%%%+%-%*%?%[%]%^%$])", "%%%1"))
end

local STATUS_SEMANTIC_PHRASES = {
	{ "已开启", GF.CHAT_STATUS_CONFIRM_COLOR_CODE },
	{ "已启用", GF.CHAT_STATUS_CONFIRM_COLOR_CODE },
	{ "确认", GF.CHAT_STATUS_CONFIRM_COLOR_CODE },
	{ "成功", GF.CHAT_STATUS_CONFIRM_COLOR_CODE },
	{ "是", GF.CHAT_STATUS_CONFIRM_COLOR_CODE },
	{ "已关闭", GF.CHAT_STATUS_FAILURE_COLOR_CODE },
	{ "已禁用", GF.CHAT_STATUS_FAILURE_COLOR_CODE },
	{ "不可用", GF.CHAT_STATUS_FAILURE_COLOR_CODE },
	{ "失败", GF.CHAT_STATUS_FAILURE_COLOR_CODE },
	{ "否", GF.CHAT_STATUS_FAILURE_COLOR_CODE },
}

local STATUS_SEMANTIC_WORDS = {
	{ "enabled", GF.CHAT_STATUS_CONFIRM_COLOR_CODE },
	{ "on", GF.CHAT_STATUS_CONFIRM_COLOR_CODE },
	{ "confirm", GF.CHAT_STATUS_CONFIRM_COLOR_CODE },
	{ "confirmed", GF.CHAT_STATUS_CONFIRM_COLOR_CODE },
	{ "success", GF.CHAT_STATUS_CONFIRM_COLOR_CODE },
	{ "yes", GF.CHAT_STATUS_CONFIRM_COLOR_CODE },
	{ "disabled", GF.CHAT_STATUS_FAILURE_COLOR_CODE },
	{ "off", GF.CHAT_STATUS_FAILURE_COLOR_CODE },
	{ "unavailable", GF.CHAT_STATUS_FAILURE_COLOR_CODE },
	{ "failed", GF.CHAT_STATUS_FAILURE_COLOR_CODE },
	{ "failure", GF.CHAT_STATUS_FAILURE_COLOR_CODE },
	{ "no", GF.CHAT_STATUS_FAILURE_COLOR_CODE },
}

local function colorStatusSemantics(body)
	body = tostring(body or "")
	if body:find("|c", 1, true) or body:find("|r", 1, true) then
		return colorWrap(GF.CHAT_STATUS_BODY_COLOR_CODE, body)
	end
	local bodyColor = GF.CHAT_STATUS_BODY_COLOR_CODE or ""
	local resetToBody = (GF.CHAT_RESET_COLOR_CODE or "|r") .. bodyColor
	for _, rule in ipairs(STATUS_SEMANTIC_PHRASES) do
		local phrase, colorCode = rule[1], rule[2]
		body = body:gsub(escapePattern(phrase), (colorCode or "") .. phrase .. resetToBody)
	end
	for _, rule in ipairs(STATUS_SEMANTIC_WORDS) do
		local word, colorCode = rule[1], rule[2]
		local function replace(match)
			return (colorCode or "") .. match .. resetToBody
		end
		local pattern = "%f[%a]" .. word .. "%f[%A]"
		body = body:gsub(pattern, replace)
		local capitalized = word:sub(1, 1):upper() .. word:sub(2)
		body = body:gsub("%f[%a]" .. capitalized .. "%f[%A]", replace)
	end
	return bodyColor .. body .. (GF.CHAT_RESET_COLOR_CODE or "|r")
end

function GF.FormatWarningMessage(msg)
	if not msg or msg == "" then
		return nil
	end
	local prefix, body = splitAddonChatMessage(msg)
	return colorWrap(GF.CHAT_WARNING_PREFIX_COLOR_CODE, prefix)
		.. colorWrap(GF.CHAT_WARNING_BODY_COLOR_CODE, body)
end

function GF.ShowWarningMessage(msg, frame)
	local text = GF.FormatWarningMessage(msg)
	if not text then
		return
	end
	frame = frame or UIErrorsFrame
	if frame and frame.AddMessage then
		local color = GF.WARNING_COLOR or { 1, 0.82, 0, 1 }
		frame:AddMessage(text, color[1] or 1, color[2] or 0.5, color[3] or 1, color[4] or 1)
	end
end

function GF.FormatStatusMessage(msg, opts)
	if not msg or msg == "" then
		return nil
	end
	opts = opts or {}
	local prefix, body = splitAddonChatMessage(msg)
	local bodyText = opts.semantic and colorStatusSemantics(body) or colorWrap(GF.CHAT_STATUS_BODY_COLOR_CODE, body)
	return colorWrap(GF.CHAT_STATUS_PREFIX_COLOR_CODE, prefix) .. bodyText
end

function GF.ShowStatusMessage(msg, opts)
	local text = GF.FormatStatusMessage(msg, opts)
	if not text then
		return
	end
	opts = opts or {}
	local frame = opts.frame or DEFAULT_CHAT_FRAME
	if frame and frame.AddMessage then
		local color = GF.STATUS_MESSAGE_COLOR or { 1, 1, 1, 1 }
		frame:AddMessage(text, color[1] or 1, color[2] or 1, color[3] or 1, color[4] or 1)
	end
end
GF.MAIN_WINDOW_LOGO_TEXTURE = GF.ADDON_LOGO_TEXTURE
GF.MAIN_WINDOW_LOGO_SIZE = 54
GF.MINIMAP_ICON_TEXTURE = GF.ADDON_LOGO_TEXTURE
GF.FLOAT_BUTTON_ICON_TEXTURE = GF.ADDON_MENU_LOGO_TEXTURE
GF.CONTEXT_MENU_TEXT_SIZE_EXTRA = 4
GF.ROLE_ICON_ATLAS = {
	LEADER = "UI-LFG-RoleIcon-Leader",
	GUIDE = "UI-LFG-RoleIcon-Leader",
	TANK = "UI-LFG-RoleIcon-Tank",
	HEALER = "UI-LFG-RoleIcon-Healer",
	DAMAGER = "UI-LFG-RoleIcon-DPS",
	DPS = "UI-LFG-RoleIcon-DPS",
	NONE = "groupfinder-icon-emptyslot",
	DEFAULT = "groupfinder-icon-emptyslot",
}
GF.SEASON_DUNGEON_ROLE_ATLAS = GF.ROLE_ICON_ATLAS
GF.LEADER_ICON_ATLAS = GF.ROLE_ICON_ATLAS.LEADER
GF.TOOLTIP_LEADER_ICON_ATLAS = GF.LEADER_ICON_ATLAS
GF.ROLE_ICON_SIZE = 18
GF.TOOLTIP_ROLE_ICON_SIZE = GF.ROLE_ICON_SIZE
GF.NON_ROLE_ICON_SIZE = 18
GF.SOCIAL_TYPE_BNET = "bnet"
GF.SOCIAL_TYPE_GUILD = "guild"
GF.SOCIAL_TYPE_FRIEND = "friend"
GF.SOCIAL_ROW_VISUAL_STATE = "blue"
GF.SOCIAL_SORT_PIN = 0
GF.NORMAL_SORT_PIN = 1
GF.SOCIAL_TEXT_COLOR = { r = 0.35, g = 0.75, b = 1 }
GF.SOCIAL_ICON_TEXTURE = GF.ADDON_ART_UI_PATH .. "Icon\\BattleNet.png"
GF.SOCIAL_TYPE_ICON_TEXTURE = {
	[GF.SOCIAL_TYPE_BNET] = GF.SOCIAL_ICON_TEXTURE,
	[GF.SOCIAL_TYPE_GUILD] = GF.SOCIAL_ICON_TEXTURE,
	[GF.SOCIAL_TYPE_FRIEND] = GF.SOCIAL_ICON_TEXTURE,
}
GF.SOCIAL_SEARCH_RESULT_LABEL_KEY = {
	[GF.SOCIAL_TYPE_BNET] = "TYPE_BNET_FRIEND",
	[GF.SOCIAL_TYPE_GUILD] = "TYPE_GUILD_FRIEND",
	[GF.SOCIAL_TYPE_FRIEND] = "TYPE_CHAR_FRIEND",
}
GF.SOCIAL_APPLICANT_LABEL_KEY = {
	[GF.SOCIAL_TYPE_BNET] = "APPLICANT_TYPE_BNET",
	[GF.SOCIAL_TYPE_GUILD] = "APPLICANT_TYPE_GUILD",
	[GF.SOCIAL_TYPE_FRIEND] = "APPLICANT_TYPE_FRIEND",
}
GF.SOCIAL_RELATIONSHIP_TYPE_BY_STRING = {
	bnet = GF.SOCIAL_TYPE_BNET,
	battlenet = GF.SOCIAL_TYPE_BNET,
	["battle.net"] = GF.SOCIAL_TYPE_BNET,
	["battle net"] = GF.SOCIAL_TYPE_BNET,
	friend = GF.SOCIAL_TYPE_FRIEND,
	charfriend = GF.SOCIAL_TYPE_FRIEND,
	characterfriend = GF.SOCIAL_TYPE_FRIEND,
	guild = GF.SOCIAL_TYPE_GUILD,
	club = GF.SOCIAL_TYPE_GUILD,
}

function GF.GetSocialRelationshipType(relationship)
	if relationship == nil or relationship == false then
		return nil
	end
	if type(relationship) == "string" then
		return GF.SOCIAL_RELATIONSHIP_TYPE_BY_STRING[relationship:lower()]
	end
	local relEnum = Enum and Enum.PartyRequestJoinRelation
	if relEnum then
		if relationship == relEnum.None then
			return nil
		end
		if relationship == relEnum.Guild or relationship == relEnum.Club then
			return GF.SOCIAL_TYPE_GUILD
		end
		if relationship == relEnum.Friend then
			return GF.SOCIAL_TYPE_FRIEND
		end
	end
	if relationship ~= 0 then
		return GF.SOCIAL_TYPE_FRIEND
	end
	return nil
end

function GF.IsSocialRelationship(relationship)
	return GF.GetSocialRelationshipType(relationship) ~= nil
end

local function countSearchResultFriends(list)
	if type(list) ~= "table" then
		return 0
	end
	return #list
end

function GF.ResolveSearchResultSocialCounts(info, resultID)
	if not info then
		return 0, 0, 0
	end
	local bnet = tonumber(info.numBNetFriends) or 0
	local guild = tonumber(info.numGuildMates) or 0
	local friend = tonumber(info.numCharFriends) or 0
	if bnet > 0 or guild > 0 or friend > 0 or info._gfSocialFriendsChecked then
		return bnet, guild, friend
	end
	if not resultID or not C_LFGList or not C_LFGList.GetSearchResultFriends then
		return bnet, guild, friend
	end

	info._gfSocialFriendsChecked = true
	local ok, bNetFriends, charFriends, guildMates = pcall(C_LFGList.GetSearchResultFriends, resultID)
	if not ok then
		return bnet, guild, friend
	end

	bnet = math.max(bnet, countSearchResultFriends(bNetFriends))
	guild = math.max(guild, countSearchResultFriends(guildMates))
	friend = math.max(friend, countSearchResultFriends(charFriends))
	info.numBNetFriends = bnet
	info.numGuildMates = guild
	info.numCharFriends = friend
	return bnet, guild, friend
end

function GF.GetSearchResultSocialType(info, resultID)
	if not info then
		return nil
	end
	local bnet, guild, friend = GF.ResolveSearchResultSocialCounts(info, resultID)
	if bnet > 0 then
		return GF.SOCIAL_TYPE_BNET
	end
	if guild > 0
		or info.isGuildListing == true then
		return GF.SOCIAL_TYPE_GUILD
	end
	if friend > 0
		or info.isFriendListing == true then
		return GF.SOCIAL_TYPE_FRIEND
	end
	return nil
end

function GF.IsSocialSearchResult(info, resultID)
	return GF.GetSearchResultSocialType(info, resultID) ~= nil
end

function GF.GetSocialTypeVisualState(socialType)
	if socialType and GF.SOCIAL_TYPE_ICON_TEXTURE[socialType] then
		return GF.SOCIAL_ROW_VISUAL_STATE
	end
	return nil
end

function GF.GetSocialSortPin(socialType)
	return GF.GetSocialTypeVisualState(socialType) and GF.SOCIAL_SORT_PIN or GF.NORMAL_SORT_PIN
end

function GF.GetSearchResultSocialSortPin(info, resultID)
	return GF.GetSocialSortPin(GF.GetSearchResultSocialType(info, resultID))
end
-- 主窗口底栏控件（活动数、屏蔽列表按钮等）锚点
GF.FRAME_BODY_BAR_LEFT = 12
GF.FRAME_BODY_BAR_Y = 6
-- SettingsFrameTemplate 内缘（与 Bar OptionsFrame / 暴雪 FlatPanel Bg 一致）
GF.FRAME_BG_INSET_LEFT = 7
GF.FRAME_BG_INSET_TOP = -18
GF.FRAME_BG_INSET_RIGHT = -2
GF.FRAME_BG_INSET_BOTTOM = 3
GF.NAV_WIDTH = 180
GF.NAV_WIDTH_MIN = 180
GF.NAV_WIDTH_MAX = 180
GF.NAV_DIVIDER_GRIP_W = 6
-- 导航竖线（§3.4，改一处即联动）
GF.NAV_DIVIDER_W = 3
GF.NAV_DIVIDER_OFFSET_X = -3
GF.NAV_DIVIDER_TOP_OFFSET = 4
GF.NAV_DIVIDER_BOTTOM_OFFSET = 2
GF.NAV_DIVIDER_COLOR = { 0.38, 0.34, 0.24, 0.9 }
GF.NAV_DIVIDER_HIGHLIGHT_COLOR = { 0.62, 0.55, 0.38, 0.3 }
GF.NAV_DIVIDER_SHADOW_COLOR = { 0.08, 0.07, 0.05, 0.9 }
GF.NAV_DIVIDER_CENTER_ACCENT_W = 1
GF.NAV_DIVIDER_CENTER_ACCENT_COLOR = { 1, 0.82, 0 }
GF.NAV_LIST_PADDING_TOP = 8
GF.BROWSE_HEADER_TOP_OFFSET = -20
GF.BROWSE_HEADER_BOTTOM_OFFSET = -46
GF.BROWSE_HEADER_CONTENT_INSET_X = 4
GF.BROWSE_HEADER_CONTENT_OFFSET_Y = 4
GF.BROWSE_HEADER_LIST_GAP = 0
GF.SECTION_HEADER_TEXT_SIZE = 14
GF.BROWSE_HEADER_TEXT_SIZE = GF.SECTION_HEADER_TEXT_SIZE
GF.BROWSE_HEADER_TEXT_INSET_X = 3
GF.BROWSE_HEADER_TEXT_PRESSED_OFFSET_X = 1
GF.BROWSE_HEADER_TEXT_PRESSED_OFFSET_Y = -1
GF.BROWSE_HEADER_TEXT_PRESSED_ALPHA = 1
GF.BROWSE_HEADER_TEXT_COLOR = { 1, 0.82, 0, 1 }
GF.BROWSE_HEADER_HOVER_TEXT_COLOR = { 1, 0.96, 0.58, 1 }
GF.BROWSE_HEADER_PRESSED_TEXT_COLOR = { 0.95, 0.68, 0.18, 1 }
GF.BROWSE_HEADER_BACKGROUND_ATLAS = "housefinder_header-bg-gradient"
GF.BROWSE_HEADER_BACKGROUND_INSET_L = -8
GF.BROWSE_HEADER_BACKGROUND_FRAME_LEVEL_OFFSET = 1
GF.BROWSE_CONTROL_BACKGROUND_ATLAS = GF.BROWSE_HEADER_BACKGROUND_ATLAS
GF.BROWSE_CONTROL_BACKGROUND_ALPHA = 1
GF.BROWSE_HEADER_ACCENT_WIDTH = 1.5
GF.BROWSE_HEADER_ACCENT_HEIGHT = 22
GF.BROWSE_HEADER_ACCENT_COLOR = { 126 / 255, 112 / 255, 82 / 255 }
GF.BROWSE_HEADER_ACCENT_ALPHA = 0.6
GF.BROWSE_HEADER_ACCENT_TEXTURE = "Interface\\Buttons\\WHITE8X8"
GF.BROWSE_EMPTY_TEXT_SIZE = 14
GF.BROWSE_HEADER_REFRESH_TEXTURE = GF.REFRESH_TEXTURE
GF.BROWSE_HEADER_REFRESH_BUTTON_SIZE = 35
GF.BROWSE_HEADER_REFRESH_ICON_SIZE = 21
GF.BROWSE_HEADER_REFRESH_ICON_PRESSED_SIZE = 19
GF.BROWSE_HEADER_REFRESH_BUTTON_OFFSET_Y = 6
GF.APPLICANT_HEADER_REFRESH_BUTTON_OFFSET_Y = 0
GF.BROWSE_LOADING_TEAMUP_TEXTURE = GF.TEAMUP_TEXTURE
GF.BROWSE_LOADING_ICON_COUNT = 3
GF.BROWSE_LOADING_ICON_WIDTH = 27
GF.BROWSE_LOADING_ICON_HEIGHT = 25
GF.BROWSE_LOADING_ICON_GAP = 6
GF.BROWSE_LOADING_TEXT_GAP = 7
GF.BROWSE_LOADING_STEP_SECONDS = 0.28
GF.BROWSE_LOADING_FADE_IN_SECONDS = 0.16
GF.BROWSE_LOADING_HOLD_SECONDS = 0.4
GF.BROWSE_LOADING_FADE_OUT_SECONDS = 0.45
GF.BROWSE_ROW_TEXTURE_NORMAL = "Interface\\AddOns\\GroupFinder\\Art\\UI\\ApplicantRowNormal.png"
GF.BROWSE_ROW_TEXTURE_GREEN = "Interface\\AddOns\\GroupFinder\\Art\\UI\\ApplicantRowGreen.png"
GF.BROWSE_ROW_TEXTURE_RED = "Interface\\AddOns\\GroupFinder\\Art\\UI\\ApplicantRowRed.png"
GF.BROWSE_ROW_TEXTURE_BLUE = "Interface\\AddOns\\GroupFinder\\Art\\UI\\ApplicantRowBlue.png"
GF.BROWSE_ROW_TEXTURE_GREY = "Interface\\AddOns\\GroupFinder\\Art\\UI\\ApplicantRowGrey.png"
GF.BROWSE_ROW_BACKGROUND_ALPHA = 0.92
GF.BROWSE_ROW_BACKGROUND_FADE_SECONDS = 0.16
GF.BROWSE_ROW_HOVER_HEIGHT = 28
GF.BROWSE_ROW_HOVER_INSET_X = 2
GF.BROWSE_ROW_HOVER_OFFSET_Y = 0
GF.BROWSE_ROW_HOVER_FADE_WIDTH = 96
GF.BROWSE_ROW_HOVER_COLOR = { 1, 0.74, 0.18, 0.13 }
GF.BROWSE_ROW_HOVER_RED_COLOR = { 1, 0.12, 0.08, 0.18 }
GF.BROWSE_ROW_HOVER_BLUE_COLOR = { 0.35, 0.75, 1, 0.16 }
GF.BROWSE_ROW_HOVER_GREY_COLOR = { 0.65, 0.65, 0.65, 0.18 }
GF.BROWSE_ROW_SELECTED_ATLAS = "groupfinder-highlightbar-yellow"
GF.BROWSE_ROW_SELECTED_BLUE_ATLAS = "groupfinder-highlightbar-blue"
GF.BROWSE_ROW_SELECTED_RED_ATLAS = "groupfinder-highlightbar-red"
GF.BROWSE_ROW_SELECTED_ALPHA = 1
GF.BROWSE_ROW_SELECTED_INSET_X = 3
GF.BROWSE_ROW_SELECTED_TOP_OFFSET_Y = -3
GF.BROWSE_ROW_SELECTED_BOTTOM_OFFSET_Y = 1
GF.CONTEXT_MENU_ROW_H = 28
GF.CONTEXT_MENU_TITLE_ROW_H = 30
GF.CONTEXT_MENU_TOP_GAP = 10
GF.CONTEXT_MENU_BOTTOM_GAP = 10
GF.CONTEXT_MENU_TITLE_BOTTOM_GAP = 4
GF.CONTEXT_MENU_MIN_W = 140
GF.CONTEXT_MENU_MAX_W = 280
GF.CONTEXT_MENU_CHROME_W = 25
GF.CONTEXT_MENU_TEXT_W_PAD = 32
GF.CONTEXT_MENU_HIGHLIGHT_TEXTURE = GF.BROWSE_ROW_TEXTURE_NORMAL
GF.CONTEXT_MENU_HIGHLIGHT_TEXTURE_W = 564
GF.CONTEXT_MENU_HIGHLIGHT_TEXTURE_H = 52
GF.CONTEXT_MENU_HIGHLIGHT_CAP_W = 36
GF.CONTEXT_MENU_HIGHLIGHT_INSET_X = 2
GF.CONTEXT_MENU_HIGHLIGHT_EXTEND_X = 0
GF.CONTEXT_MENU_HIGHLIGHT_OFFSET_X = -12
GF.CONTEXT_MENU_HIGHLIGHT_ALPHA = 1
GF.BROWSE_ROW_MEMBER_ICON_SIZE = GF.ROLE_ICON_SIZE
GF.BROWSE_ROW_MEMBER_ICON_GAP = 2
GF.BROWSE_ROW_MEMBER_MAX_ICONS = 5
GF.BROWSE_ROW_MEMBER_EMPTY_SLOT_ATLAS = GF.ROLE_ICON_ATLAS.DEFAULT
GF.BROWSE_ROW_MEMBER_ROLE_BADGE_SIZE = 12
GF.BROWSE_ROW_MEMBER_ROLE_BADGE_OFFSET_X = 2
GF.BROWSE_ROW_MEMBER_ROLE_BADGE_OFFSET_Y = 2
GF.BROWSE_ROW_MEMBER_LEADER_BADGE_SIZE = GF.BROWSE_ROW_MEMBER_ROLE_BADGE_SIZE
GF.BROWSE_ROW_MEMBER_LEADER_BADGE_OFFSET_X = GF.BROWSE_ROW_MEMBER_ROLE_BADGE_OFFSET_X
GF.BROWSE_ROW_MEMBER_LEADER_BADGE_OFFSET_Y = GF.BROWSE_ROW_MEMBER_ROLE_BADGE_OFFSET_Y
GF.MEMBER_DISPLAY_MODE_ROLE = "role"
GF.MEMBER_DISPLAY_MODE_SPEC = "spec"
GF.MEMBER_DISPLAY_MODE_DEFAULT = GF.MEMBER_DISPLAY_MODE_ROLE
GF.MEMBER_TOOLTIP_MODE_DETAILS = "details"
GF.MEMBER_TOOLTIP_MODE_SPEC_COUNT = "spec_count"
GF.MEMBER_TOOLTIP_MODE_DEFAULT = GF.MEMBER_TOOLTIP_MODE_DETAILS
GF.BROWSE_COLUMN_PRESET_VERSION = 5
GF.APPLICANT_COLUMN_PRESET_VERSION = 6
GF.APPLICANT_ROW_H = 33
GF.APPLICANT_ROW_BACKGROUND_INSET_TOP = 2
GF.APPLICANT_ROW_BACKGROUND_INSET_BOTTOM = 0
GF.APPLICANT_ROW_CONTENT_OFFSET_Y = -1
GF.APPLICANT_ACTION_BUTTON_SIZE = 24
GF.APPLICANT_ACTION_ICON_SIZE = 12
GF.SUBTITLE_H = 42
GF.SUBTITLE_TOOLBAR_H = 26
GF.SUBTITLE_TOOLBAR_HEADER_GAP = 10
GF.SUBTITLE_HEADER_H = 26
GF.BROWSE_CONTROL_BACKGROUND_H = GF.SUBTITLE_HEADER_H
GF.BROWSE_CONTROL_BACKGROUND_OFFSET_Y = 4
GF.SUBTITLE_CONTROL_CENTER_OFFSET_Y =
	((GF.SUBTITLE_H + GF.BROWSE_CONTROL_BACKGROUND_OFFSET_Y) / 2) - (GF.SUBTITLE_H / 2)
GF.SUBTITLE_CATEGORY_ACCENT_GAP = 10
GF.SUBTITLE_CATEGORY_MIN_W = 140
GF.SUBTITLE_CONTROL_LEFT_PAD = 15
GF.SUBTITLE_CONTROL_RIGHT_PAD = 10
GF.SUBTITLE_CONTROL_GAP = 7
GF.SUBTITLE_CONTROL_TOP_OFFSET = -8
GF.SUBTITLE_SEARCH_W = 220
GF.SUBTITLE_SEARCH_H = 26
GF.SUBTITLE_SEARCH_TEXT_SIZE = 12
GF.SUBTITLE_SEARCH_ICON_INSET = 8
GF.SUBTITLE_SEARCH_ICON_SIZE = 14
GF.SUBTITLE_SEARCH_TEXT_INSET_LEFT = 27
GF.SUBTITLE_SEARCH_TEXT_INSET_RIGHT = 24
GF.SUBTITLE_SEARCH_CLEAR_INSET = 6
GF.SUBTITLE_SEARCH_TEXT_COLOR = { 1, 0.96, 0.86, 1 }
GF.SUBTITLE_SEARCH_PLACEHOLDER_COLOR = { 0.55, 0.55, 0.55, 1 }
GF.SUBTITLE_SEARCH_BACKGROUND_COLOR = { 0, 0, 0, 0.55 }
GF.SUBTITLE_SEARCH_BACKGROUND_INSET_X = 3
GF.SUBTITLE_SEARCH_BACKGROUND_INSET_Y = 3
GF.SUBTITLE_SEARCH_BORDER_ATLAS = "common-dropdown-textholder"
GF.SUBTITLE_SEARCH_OWNERSHIP_POLL_SEC = 0.5
GF.SUBTITLE_OPTION_CHECK_SIZE = 22
GF.SUBTITLE_OPTION_TEXT_SIZE = 13
GF.SUBTITLE_CATEGORY_TEXT_SIZE = 13
GF.SUBTITLE_OPTION_TEXT_GAP = 1
GF.SUBTITLE_OPTION_GROUP_GAP = 12
GF.SUBTITLE_OPTION_TEXT_COLOR = { 1, 0.82, 0, 1 }
GF.FILTER_FOOTER_H = GF.SUBTITLE_H
GF.FILTER_FOOTER_INSET_L = GF.FRAME_BG_INSET_LEFT
GF.FILTER_FOOTER_INSET_R = GF.FRAME_BG_INSET_RIGHT
GF.FILTER_FOOTER_INSET_B = GF.FRAME_BG_INSET_BOTTOM
GF.FILTER_FOOTER_BUTTON_INSET_X = 12
GF.FILTER_FOOTER_BUTTON_OFFSET_Y = 12
GF.FILTER_FOOTER_BUTTON_GAP = 10
GF.FILTER_SCROLLBAR_WIDTH = 8
GF.FILTER_SCROLLBAR_RIGHT_INSET = 8
GF.FILTER_SCROLLBAR_TOP_INSET = 8
GF.FILTER_SCROLLBAR_BOTTOM_INSET = 8
GF.FILTER_TEXT_COLOR = GF.SUBTITLE_OPTION_TEXT_COLOR
GF.SIDE_PANEL_W = 260
GF.SATELLITE_PANEL_GAP = 2
GF.FILTER_PANEL_W = GF.SIDE_PANEL_W
GF.FILTER_CHECK_SIZE = 20
GF.FILTER_CHECK_LABEL_GAP = 6
GF.FILTER_CHECK_ATLAS_INSET_X = 0.5 / 128
GF.FILTER_CHECK_ATLAS_INSET_Y = 0.5 / 64
GF.PANEL_BUTTON_H = 24
GF.PANEL_BUTTON_STANDARD_W = 72
GF.PANEL_BUTTON_TWO_CHAR_W = GF.PANEL_BUTTON_STANDARD_W
GF.PANEL_BUTTON_ACTION_W = GF.PANEL_BUTTON_STANDARD_W
GF.PANEL_CONFIRM_BUTTON_W = GF.PANEL_BUTTON_STANDARD_W
GF.APPLICANT_MANAGE_BUTTON_W = GF.PANEL_BUTTON_STANDARD_W
GF.NAV_INDENT = 12
GF.NAV_L0_ROW_H = 45
GF.NAV_L0_BUTTON_H = 43
GF.NAV_ROW_H = { [0] = GF.NAV_L0_ROW_H, [1] = 38, [2] = 34, [3] = 30 }
GF.NAV_TRANSMOG_BUTTON_H = { [0] = GF.NAV_L0_BUTTON_H, [1] = 34, [2] = 30, [3] = 26 }
GF.NAV_TRANSMOG_BUTTON_OFFSET_X = 0
GF.NAV_TRANSMOG_BUTTON_WIDTH_PAD = { [0] = 20, [1] = 32, [2] = 44, [3] = 56 }
GF.NAV_TRANSMOG_BUTTON_TEXT_LEFT = { [0] = 12, [1] = 14, [2] = 16, [3] = 18 }
GF.NAV_TRANSMOG_BUTTON_TEXT_RIGHT = 8
GF.NAV_L0_ROW_SPACING = 2
GF.NAV_ROW_H_DEFAULT = 28
GF.NAV_FLYOUT_PANEL_MIN_W = 120
GF.NAV_FLYOUT_PANEL_MAX_W = 260
GF.NAV_FLYOUT_ROOT_GAP = 2
GF.NAV_FLYOUT_ROOT_ANCHOR_GAP_X = 4
GF.NAV_FLYOUT_ROOT_ANCHOR_GAP_Y = 2
GF.NAV_FLYOUT_PANEL_GAP = 6
GF.NAV_FLYOUT_STAIR_STEP_Y = 9
GF.NAV_FLYOUT_SCREEN_MARGIN_TOP = 12
GF.NAV_FLYOUT_WINDOW_BOTTOM_GAP = 0
GF.NAV_FLYOUT_ROW_H = { [1] = 32, [2] = 32, [3] = 30 }
GF.NAV_FLYOUT_ROW_H_DEFAULT = 30
GF.NAV_FLYOUT_ROW_TEXT_L = 12
GF.NAV_FLYOUT_ROW_TEXT_R = 10
GF.NAV_FLYOUT_ARROW_ATLAS = "bag-arrow"
GF.NAV_FLYOUT_ARROW_W = 10
GF.NAV_FLYOUT_ARROW_H = 16
GF.NAV_FLYOUT_ARROW_R = 8
GF.NAV_FLYOUT_ARROW_RESERVE_W = 28
GF.NAV_FLYOUT_LABEL_EXTRA_W = 8
GF.NAV_FLYOUT_HIGHLIGHT_INSET_X = 3
GF.NAV_FLYOUT_HIGHLIGHT_INSET_TOP = 3
GF.NAV_FLYOUT_HIGHLIGHT_INSET_BOTTOM = 1
GF.NAV_FLYOUT_ROW_TEXTURE_EXTEND_X = 8
GF.NAV_FLYOUT_SELECTED_INSET_TOP = 5
GF.NAV_FLYOUT_SELECTED_INSET_BOTTOM = 5
GF.NAV_FLYOUT_SELECTED_EXTEND_BOTTOM = 0.5
GF.NAV_FLYOUT_HOVER_TEXTURE = GF.BROWSE_ROW_TEXTURE_NORMAL
GF.NAV_FLYOUT_HOVER_TEXTURE_W = 564
GF.NAV_FLYOUT_HOVER_TEXTURE_H = 52
GF.NAV_FLYOUT_HOVER_CAP_W = 36
GF.NAV_FLYOUT_HOVER_ALPHA = 1
GF.NAV_FLYOUT_SELECTED_ATLAS = "groupfinder-highlightbar-yellow"
GF.NAV_FLYOUT_BG_ATLAS = "common-dropdown-bg"
GF.NAV_FLYOUT_BG_EXTEND_X = 10
GF.NAV_FLYOUT_BG_EXTEND_TOP = 3
GF.NAV_FLYOUT_BG_EXTEND_BOTTOM = 3
GF.NAV_FLYOUT_BG_ALPHA = 0.925
GF.NAV_FLYOUT_CONTENT_INSET_L = 8
GF.NAV_FLYOUT_CONTENT_INSET_R = 8
GF.NAV_FLYOUT_CONTENT_INSET_T = 8
GF.NAV_FLYOUT_CONTENT_INSET_B = 15
GF.LIST_ROW_H = 32
GF.LIST_ROW_H_DEFAULT = 32
GF.BROWSE_TEXT_CELL_INSET_X = 2
GF.ROLE_COUNT_ICON_DEFAULT = GF.ROLE_ICON_SIZE
GF.ROLE_COUNT_NUM_ICON_GAP = 5
GF.COL_MIN_WIDTH_MIN = 20
GF.COL_MIN_WIDTH_MAX = 300
GF.COL_WEIGHT_MIN = 0
GF.COL_WEIGHT_MAX = 3
-- 内容区 scroll 右缘留白 + 滚动条贴窗右（各内容页共用，仅改此处）
GF.CONTENT_SCROLL_INSET_L = 5
GF.CONTENT_SCROLL_INSET_R = 18
GF.CONTENT_SCROLL_INSET_B = 0
GF.CONTENT_SCROLLBAR_OFFSET_X = 9
-- 内容区相对 nav 右缘水平偏移（负=向左靠分割线）
GF.CONTENT_NAV_OFFSET_X = 0
GF.LIST_CONTENT_EDGE_PAD = 18
GF.LIST_CONTENT_EDGE_PAD_MAX = 30
-- 列表行四列左边界（在 LIST_COL_REF_W 参考行宽下的 px）
GF.LIST_COL_REF_W = 930
GF.ACTIVITY_COUNT_RIGHT = 28
GF.LIST_WHEEL_ROWS_MIN = 1
GF.LIST_WHEEL_ROWS_MAX = 10
GF.LIST_WHEEL_ROWS_DEFAULT = 3

GF.INVITE_CAP_MIN = 1
GF.INVITE_CAP_MAX = 40
GF.INVITE_CAP_DEFAULT = 40
GF.NAV_WHEEL_ROW_H = 28
GF.SETTINGS_WHEEL_ROW_H = 24
GF.SETTINGS_COL_W = 280
GF.SETTINGS_GUTTER_MIN = 50
GF.SETTINGS_GUTTER_WEIGHT = 1
-- host 左缘到石纹内缘；右缘 CONTENT_SCROLL_INSET_R 为滚动条走廊
GF.SETTINGS_LAYOUT_INSET_L = (GF.FRAME_BG_INSET_LEFT or 7) - (GF.FRAME_PAD or 4)
GF.SETTINGS_LAYOUT_INSET_R = GF.CONTENT_SCROLL_INSET_R or 18
-- WowStyle1Dropdown 等控件相对列 Frame 左缘的视觉外溢
GF.SETTINGS_COL_EDGE_INSET_L = 10
-- 设置页控件行距：下拉/滑块底留白 8 + 分区表头前 12 + 表头内锚 8 = 表头上方 28px
GF.SETTINGS_ROW_BOTTOM_PAD = 8
GF.SETTINGS_SECTION_TOP_PAD = 12
GF.SETTINGS_DROPDOWN_W = 260
GF.SETTINGS_DROPDOWN_H = 26
GF.SETTINGS_SLIDER_W = 220
GF.SETTINGS_SLIDER_H = 19
GF.FILTER_WHEEL_ROW_H = 28
GF.BLOCKLIST_WHEEL_ROW_H = 28
-- 屏蔽列表列左边界（参考行宽 900；第 1 列文字在 +/- 占位与勾选框之后）
GF.BLOCKLIST_COL_X = { 62, 380, 560 }
GF.APPLICANTS_WHEEL_ROW_H = 30
GF.CREATE_DRAWER_W = 600
GF.CREATE_DRAWER_H = 390
GF.CREATE_DRAWER_HORIZONTAL_MIN_W = 560
GF.CREATE_FORM_COLUMN_GAP = 16
GF.CREATE_DRAWER_AVOID_ALPHA = 0.64
GF.CREATE_DRAWER_MAIN_ALPHA = GF.CREATE_DRAWER_AVOID_ALPHA
GF.CREATE_WHEEL_ROW_H = 24
GF.LIST_REFRESH_DEBOUNCE = 0.2
GF.BROWSE_LISTEN_HOLD_SEC = 60
GF.BROWSE_SORT_KEY_BATCH = 25
GF.FILTER_BATCH = 25
GF.ROW_UPDATE_DEBOUNCE = 0.2
GF.LAYOUT_RESIZE_DEBOUNCE = 0.1
GF.SEARCH_COOLDOWN = 3
GF.SEARCH_TIMEOUT_SECONDS = 8

GF.TAB_BROWSE = 1
GF.TAB_CREATE = 2
GF.TAB_BLOCKLIST = 3
GF.TAB_SETTINGS = 4
GF.TAB_DEBUG = 5


GF.APPLY_MANUAL = "manual"
GF.APPLY_CLICK_CONFIRM = "click_confirm"
GF.APPLY_DBLCLICK_AUTO = "dblclick_auto"

GF.LIST_HOVER_TOOLTIP_HIDE_DELAY = 0.1

GF.FRAME_STRATA_DEFAULT = "MEDIUM"
GF.FRAME_STRATA_CHOICES = { "LOW", "MEDIUM", "HIGH", "DIALOG" }

GF.CAT_QUEST = 1
GF.CAT_DUNGEON = 2
GF.CAT_RAID = 3
GF.CAT_CUSTOM = 6
GF.ACTIVITY_CUSTOM_PVE = 16
GF.ACTIVITY_HOUSEWARMING = 1972

GF.BODY_BACKGROUND_COLOR = { 0.05, 0.05, 0.08, 0.75 }
GF.CAT_DELVE = 121

GF.PVP_CATEGORIES = {
	{ id = 8, key = "PVP_BG" },
	{ id = 4, key = "PVP_ARENA" },
	{ id = 9, key = "PVP_RATED" },
	{ id = 7, key = "PVP_SKIRMISH" },
}

GF.SEL_TEX = "Interface\\PVPFrame\\PvPMegaQueue"
GF.SEL_TEX_HOVER = { 0.00195313, 0.63867188, 0.70703125, 0.76757813 }
GF.SEL_TEX_SELECTED = { 0.00195313, 0.63867188, 0.76953125, 0.83007813 }

-- 列表 scroll 内容区右留白：参考宽以下随 scroll 变窄而增大（仅 resize 时算一次）
function GF.GetListContentEdgePad(scrollW)
	local base = GF.LIST_CONTENT_EDGE_PAD or 18
	local maxPad = GF.LIST_CONTENT_EDGE_PAD_MAX or 30
	local refW = GF.LIST_COL_REF_W or 900
	if not scrollW or scrollW <= 0 or scrollW >= refW then
		return base
	end
	local pad = math.floor(base * refW / scrollW)
	if pad < base then
		pad = base
	elseif pad > maxPad then
		pad = maxPad
	end
	return pad
end

-- 设置页双列水平布局：相对内容 host 内缘（非 scroll 视口）；三槽各不低于 SETTINGS_GUTTER_MIN
function GF.ResolveSettingsColumnLayout(hostW)
	local colW = GF.SETTINGS_COL_W or 280
	local gutterMin = GF.SETTINGS_GUTTER_MIN or 50
	local gutterWeight = GF.SETTINGS_GUTTER_WEIGHT or 1
	local gutterCount = 3

	hostW = math.max(0, math.floor(hostW or 0))
	local pool = hostW - 2 * colW
	if pool <= 0 then
		return 0, 0
	end

	local minSum = gutterCount * gutterMin
	local leftPad, midGap, rightPad

	if pool <= minSum then
		local scale = pool / minSum
		leftPad = math.max(0, math.floor(gutterMin * scale))
		midGap = math.max(0, math.floor(gutterMin * scale))
		rightPad = math.max(0, pool - leftPad - midGap)
	else
		leftPad = gutterMin
		midGap = gutterMin
		rightPad = gutterMin
		local extra = pool - minSum
		local weightSum = gutterCount * gutterWeight
		if extra > 0 and weightSum > 0 then
			local assigned = 0
			local adds = { 0, 0, 0 }
			for i = 1, gutterCount do
				local add = math.floor(extra * gutterWeight / weightSum)
				adds[i] = add
				assigned = assigned + add
			end
			leftPad = leftPad + adds[1]
			midGap = midGap + adds[2]
			rightPad = rightPad + adds[3]
			local remain = extra - assigned
			local idx = 1
			while remain > 0 do
				if idx == 1 then
					leftPad = leftPad + 1
				elseif idx == 2 then
					midGap = midGap + 1
				else
					rightPad = rightPad + 1
				end
				remain = remain - 1
				idx = idx + 1
				if idx > gutterCount then
					idx = 1
				end
			end
		end
	end

	local leftX = leftPad + (GF.SETTINGS_COL_EDGE_INSET_L or 0)
	local rightX = leftPad + colW + midGap
	return leftX, rightX
end

function GF.GetBrowseListLayoutWidth()
	if GF.FindGroupTab and GF.FindGroupTab.GetLayoutWidth then
		return GF.FindGroupTab:GetLayoutWidth()
	end
	return 1
end

function GF.GetApplicantListLayoutWidth()
	local ap = GF.ApplicantsPanel
	if ap and ap.scrollList then
		return ap.scrollList:GetLayoutWidth()
	end
	return 1
end
