Quantcast
local _G = _G
local PitBull4 = _G.PitBull4

local DEBUG = PitBull4.DEBUG
local expect = PitBull4.expect
local deep_copy = PitBull4.Utils.deep_copy

local MAX_PARTY_MEMBERS_WITH_PLAYER = MAX_PARTY_MEMBERS + 1
local NUM_CLASSES = #CLASS_SORT_ORDER
local MINIMUM_EXAMPLE_GROUP = 2

-- lock to prevent the SecureGroupHeader_Update for doing unnecessary
-- work when running ForceShow
local in_force_show = false

--- Make a group header.
-- @param group the name for the group. Also acts as a unique identifier.
-- @usage local header = PitBull4:MakeGroupHeader("Monkey")
function PitBull4:MakeGroupHeader(group)
	if DEBUG then
		expect(group, 'typeof', 'string')
	end

	local group_db = PitBull4.db.profile.groups[group]
	local pet_based = not not group_db.unit_group:match("pet") -- this feels dirty
	local use_pet_header = pet_based and group_db.use_pet_header
	local header_name

	if use_pet_header then
		header_name = "PitBull4_PetGroups_" .. group
	else
		header_name = "PitBull4_Groups_" .. group
	end

	local header = _G[header_name]
	if not header then
		local template
		if use_pet_header then
			template = "SecureGroupPetHeaderTemplate"
		else
			template = "SecureGroupHeaderTemplate"
		end
		header = CreateFrame("Frame", header_name, UIParent, template)
		header:Hide() -- it will be shown later and attributes being set won't cause lag

		header.name = group

		header.group_db = group_db

		self:ConvertIntoGroupHeader(header)
	elseif header.group_db ~= group_db then
		-- If the frame already exists and the group_db doesn't already match the one
		-- we expect it to be then it's a recreated frame from one we've previously
		-- deleted so we need to set the group_db and force an update.
		header.group_db = group_db
		header:RefreshGroup()
	end

	header:UpdateShownState()
end
PitBull4.MakeGroupHeader = PitBull4:OutOfCombatWrapper(PitBull4.MakeGroupHeader)

--- Swap the group from a Normal and Pet Group Header.
-- @param group the name for the group.
-- @usage PitBull4:SwapGroupTemplate("Monkey")
-- Note that the use_pet_header setting for the group_db is expected to already
-- be set to the value you're going to.
function PitBull4:SwapGroupTemplate(group)
	if DEBUG then
		expect(group, 'typeof', 'string')
	end

	local old_header = self.name_to_header[group]
	local group_db = PitBull4.db.profile.groups[group]

	old_header.group_db = deep_copy(group_db)
	old_header.group_db.enabled = false
	old_header:RefreshGroup()
	old_header:UpdateShownState()

	local new_name
	if group_db.use_pet_header then
		new_name = "PitBull4_PetGroups_"..group
	else
		new_name = "PitBull4_Groups_"..group
	end
	local new_header = _G[new_name]

	if not new_header then
		-- Doesn't exist so make it.
		self:MakeGroupHeader(group)
	else
		-- already exists so jump through the hoops to reactive it.
		self.name_to_header[group] = new_header
		new_header.group_db = group_db
		new_header:RefreshGroup()
		new_header:UpdateShownState()
	end

	self:RecheckConfigMode()
end
PitBull4.SwapGroupTemplate = PitBull4:OutOfCombatWrapper(PitBull4.SwapGroupTemplate)

local GroupHeader = {}
PitBull4.GroupHeader = GroupHeader
local GroupHeader__scripts = {}
PitBull4.GroupHeader__scripts = GroupHeader__scripts

local MemberUnitFrame = PitBull4.MemberUnitFrame
local MemberUnitFrame__scripts = PitBull4.MemberUnitFrame__scripts

--- Force an update on the group header.
-- This is just a wrapper for SecureGroupHeader_Update.
-- @usage header:Update()
function GroupHeader:Update()
	-- We can't directly call SecureGroupHeader_Update so we just
	-- set an attribute back to iself.  Calling SecureGroupHeader_Update
	-- directly taints the entire template system and is very bad.
	self:SetAttribute("maxColumns",self:GetAttribute("maxColumns"))
end
GroupHeader.Update = PitBull4:OutOfCombatWrapper(GroupHeader.Update)

--- Send :Update to all member frames.
-- @args ... the arguments to send along with :Update
-- @usage header:UpdateMembers(true, true)
function GroupHeader:UpdateMembers(...)
	for _, frame in self:IterateMembers() do
		frame:Update(...)
	end
end

function GroupHeader:ProxySetAttribute(key, value)
	if self:GetAttribute(key) ~= value then
		self:SetAttribute(key, value)
		return true
	end
end

function GroupHeader:UpdateShownState()
	local group_db = self.group_db
	if not group_db or not group_db.enabled then
		PitBull4:RemoveGroupFromStateHeader(self)
	else
		PitBull4:AddGroupToStateHeader(self)
	end
end
GroupHeader.UpdateShownState = PitBull4:OutOfCombatWrapper(GroupHeader.UpdateShownState)

local DIRECTION_TO_POINT = {
	down_right = "TOP",
	down_left = "TOP",
	up_right = "BOTTOM",
	up_left = "BOTTOM",
	right_down = "LEFT",
	right_up = "LEFT",
	left_down = "RIGHT",
	left_up = "RIGHT",
}

local DIRECTION_TO_GROUP_ANCHOR_POINT = {
	down_right = "TOPLEFT",
	down_left = "TOPRIGHT",
	up_right = "BOTTOMLEFT",
	up_left = "BOTTOMRIGHT",
	right_down = "TOPLEFT",
	right_up = "BOTTOMLEFT",
	left_down = "TOPRIGHT",
	left_up = "BOTTOMRIGHT",
}

local DIRECTION_TO_COLUMN_ANCHOR_POINT = {
	down_right = "LEFT",
	down_left = "RIGHT",
	up_right = "LEFT",
	up_left = "RIGHT",
	right_down = "TOP",
	right_up = "BOTTOM",
	left_down = "TOP",
	left_up = "BOTTOM",
}

local DIRECTION_TO_HORIZONTAL_SPACING_MULTIPLIER = {
	down_right = 1,
	down_left = -1,
	up_right = 1,
	up_left = -1,
	right_down = 1,
	right_up = 1,
	left_down = -1,
	left_up = -1,
}

local DIRECTION_TO_VERTICAL_SPACING_MULTIPLIER = {
	down_right = -1,
	down_left = -1,
	up_right = 1,
	up_left = 1,
	right_down = -1,
	right_up = 1,
	left_down = -1,
	left_up = 1,
}

local GROUPING_ORDER = {}
do
	local t = {}
	for i = 1, NUM_RAID_GROUPS do
		t[i] = i..""
	end
	GROUPING_ORDER.GROUP = table.concat(t, ',')
end
GROUPING_ORDER.CLASS = function()
	return table.concat(PitBull4.ClassOrder, ",")
end

local function position_label(self, label)
	label:ClearAllPoints()
	local group_db = self.group_db
	if group_db.direction:match("down") then
		label:SetPoint("BOTTOM", self, "TOP", 0, group_db.vertical_spacing)
	else
		label:SetPoint("TOP", self, "BOTTOM", 0, -group_db.vertical_spacing)
	end
end

--- Reset the size and position of the group header.  More accurately,
-- the scale and the position since size is set dynamically.
-- @usage header:RefixSizeAndPosition()
function GroupHeader:RefixSizeAndPosition()
	local group_db = self.group_db
	local layout = group_db.layout
	local layout_db = PitBull4.db.profile.layouts[layout]
	local updated = false

	self:SetScale(layout_db.scale * group_db.scale)
	self:SetFrameStrata(layout_db.strata)
	self:SetFrameLevel(layout_db.level - 1) -- 1 less than what the unit frame will be at

	local scale = self:GetEffectiveScale() / UIParent:GetEffectiveScale()
	local direction = group_db.direction
	local anchor = DIRECTION_TO_GROUP_ANCHOR_POINT[direction]
	local unit_width = layout_db.size_x * group_db.size_x
	local unit_height = layout_db.size_y * group_db.size_y
	local x_diff = unit_width / 2 * -DIRECTION_TO_HORIZONTAL_SPACING_MULTIPLIER[direction]
	local y_diff = unit_height / 2 * -DIRECTION_TO_VERTICAL_SPACING_MULTIPLIER[direction]

	updated = self:ProxySetAttribute('unitWidth',unit_width) or updated
	updated = self:ProxySetAttribute('unitHeight',unit_height) or updated
	updated = self:ProxySetAttribute('clickThrough',group_db.click_through) or updated

	-- Set minimum width and height.  If we don't do this then
	-- SecureTemplates will calculate the size dynamically and these
	-- dimensions will end up being set to 0.1 if there are no units to
	-- display.  This causes the positioning of the group header to move
	-- and results in group frames that jump when someone joins the group
	-- from where they were in config mode.
	updated = self:ProxySetAttribute("minWidth",unit_width) or updated
	updated = self:ProxySetAttribute("minHeight",unit_height) or updated

	if not updated then
		-- Update absolutely must be called at least once to ensure the GroupHeader
		-- frame size is recalculated.
		self:Update()
	end


	self:ClearAllPoints()
	self:SetPoint(anchor, UIParent, "CENTER", group_db.position_x / scale + x_diff, group_db.position_y / scale + y_diff)
end

local function count_returns(...)
	return select('#', ...)
end

local tank_list = {}
local function get_main_tank_name_list()
	local main_tanks
	if oRA3 then
		main_tanks = oRA3:GetSortedTanks()
	elseif oRA then
		main_tanks = oRA.maintanktable
	else
		main_tanks = CT_RA_MainTanks
	end
	if main_tanks then
		wipe(tank_list)
		for i = 1, 10 do
			local v = main_tanks[i]
			if v then
				tank_list[#tank_list+1] = v
			end
		end
		local s = table.concat(tank_list, ',')
		if s ~= "" then
			return s, #tank_list
		end
	end
	if PitBull4.leaving_world or not UnitInRaid("player") or not UnitInParty("player") then
		-- Not in a raid or a party, so no main tank list.  We have
		-- to bail out here becuase WoW whines with a You are not in a party
		-- message to the user now.  /sigh
		return nil, 0
	else
		return nil, count_returns(GetPartyAssignment("MAINTANK"))
	end
end

--- Recheck the group-based settings of the group header, including sorting, position, what units are shown.
-- @param dont_refresh_children don't call :RefreshLayout on the child frames
-- @usage header:RefreshGroup()
function GroupHeader:RefreshGroup(dont_refresh_children)
	local group_db = self.group_db

	local layout = group_db.layout
	self.layout = layout

	local layout_db = PitBull4.db.profile.layouts[layout]
	self.layout_db = layout_db

	self.dont_update = true
	for _, frame in self:IterateMembers() do
		frame.dont_update = true
	end

	local is_shown = self:IsShown()
	self:Hide()

	local force_show = self.force_show
	self:UnforceShow()

	local enabled = group_db.enabled
	local unit_group = group_db.unit_group
	local party_based = unit_group:sub(1, 5) == "party"
	local include_player = party_based and group_db.include_player
	local show_when = group_db.show_when
	local show_solo = include_player and show_when.solo
	local group_filter = not party_based and group_db.group_filter or nil
	local sort_direction = group_db.sort_direction
	local sort_method = group_db.sort_method
	local group_by = group_db.group_by
	local name_list

	if group_filter == "MAINTANK" then
		name_list = get_main_tank_name_list()
	end

	local changed_units = self.unit_group ~= unit_group or self.include_player ~= include_player or self.show_solo ~= show_solo or self.group_filter ~= group_filter or self.sort_direction ~= sort_direction or self.sort_method ~= sort_method or self.group_by ~= group_by or self.name_list ~= name_list

	if changed_units then
		local old_unit_group = self.unit_group
		local old_super_unit_group = self.super_unit_group
		self.unit_group = unit_group
		self.include_player = include_player
		self.show_solo = show_solo
		self.group_filter = group_filter
		self.sort_direction = sort_direction
		self.sort_method = sort_method
		self.group_by = group_db.group_by
		self.name_list = name_list
		if DEBUG then
			if not party_based then
				expect(unit_group:sub(1, 4), '==', "raid")
			end
		end

		if party_based then
			self.super_unit_group = "party"
			self.unitsuffix = unit_group:sub(6)
			self:SetAttribute("showRaid", nil)
			self:SetAttribute("showParty", true)
			self:SetAttribute("showPlayer", include_player and true or nil)
			self:SetAttribute("showSolo", show_solo and true or nil)
			self:SetAttribute("groupFilter", nil)
		else
			self.super_unit_group = "raid"
			self.unitsuffix = unit_group:sub(5)
			self:SetAttribute("showParty", nil)
			self:SetAttribute("showPlayer", nil)
			self:SetAttribute("showSolo", nil)
			self:SetAttribute("showRaid", true)
			if name_list then
				self:SetAttribute("groupFilter", nil)
				self:SetAttribute("nameList", name_list)
			else
				self:SetAttribute("groupFilter", group_filter)
				self:SetAttribute("nameList", nil)
			end
		end
		if self.unitsuffix == "" then
			self.unitsuffix = nil
		end

		local is_wacky = PitBull4.Utils.IsWackyUnitGroup(unit_group)
		self.is_wacky = is_wacky

		if old_unit_group then
			PitBull4.unit_group_to_headers[old_unit_group][self] = nil
			PitBull4.super_unit_group_to_headers[old_super_unit_group][self] = nil
		end

		for _, frame in self:IterateMembers() do
			frame:SetAttribute("unitsuffix", self.unitsuffix)
			frame.is_wacky = is_wacky
		end
		PitBull4.unit_group_to_headers[unit_group][self] = true
		PitBull4.super_unit_group_to_headers[self.super_unit_group][self] = true
	end

	local direction = group_db.direction
	local point = DIRECTION_TO_POINT[direction]

	self:SetAttribute("point", point)
	if point == "LEFT" or point == "RIGHT" then
		self:SetAttribute("xOffset", group_db.horizontal_spacing * DIRECTION_TO_HORIZONTAL_SPACING_MULTIPLIER[direction])
		self:SetAttribute("yOffset", 0)
		self:SetAttribute("columnSpacing", group_db.vertical_spacing)
	else
		self:SetAttribute("xOffset", 0)
		self:SetAttribute("yOffset", group_db.vertical_spacing * DIRECTION_TO_VERTICAL_SPACING_MULTIPLIER[direction])
		self:SetAttribute("columnSpacing", group_db.horizontal_spacing)
	end
	if self.label then
		position_label(self, self.label)
	end
	self:SetAttribute("sortMethod", sort_method)
	self:SetAttribute("sortDir", sort_direction)
	self:SetAttribute("template", "PitBull4_UnitTemplate_Clique")
	self:SetAttribute("templateType", "Button")
	self:SetAttribute("groupBy", group_by)
	local order = GROUPING_ORDER[group_db.group_by]
	if type(order) == "function" then
		order = order()
	end
	self:SetAttribute("groupingOrder", order)
	self:SetAttribute("unitsPerColumn", group_db.units_per_column)
	self:SetAttribute("maxColumns", self:GetMaxUnits())
	self:SetAttribute("startingIndex", 1)
	self:SetAttribute("columnAnchorPoint", DIRECTION_TO_COLUMN_ANCHOR_POINT[direction])
	self:SetAttribute("useOwnerUnit", 1)

	-- Set the attributes for the StateHeader to know when to show and hide this
	-- group
	for k,v in pairs(show_when) do
		if k == "solo" then
			self:SetAttribute(k, enabled and show_solo and party_based)
		elseif k == "party" then
			self:SetAttribute(k, enabled and v and party_based)
		else
			self:SetAttribute(k, enabled and v)
		end
	end

	self:RefixSizeAndPosition()

	if is_shown then
		self:Show()
	end

	if force_show then
		self:ForceShow()
	end

	for _, frame in self:IterateMembers() do
		frame.dont_update = nil
	end
	self.dont_update = nil

	if changed_units and not dont_refresh_children then
		for _, frame in self:IterateMembers() do
			frame:RefreshLayout()
		end
	end
end
GroupHeader.RefreshGroup = PitBull4:OutOfCombatWrapper(GroupHeader.RefreshGroup)

--- Recheck the layout of the group header, refreshing the layout of all members.
-- @param dont_refresh_children don't call :RefreshLayout on the child frames
-- @usage header:RefreshLayout()
function GroupHeader:RefreshLayout(dont_refresh_children)
	self:RefixSizeAndPosition()

	if not dont_refresh_children then
		for _, frame in self:IterateMembers() do
			frame:RefreshLayout()
		end
	end
end
GroupHeader.RefreshLayout = PitBull4:OutOfCombatWrapper(GroupHeader.RefreshLayout)

--- Initialize a member frame. This should be called once per member frame immediately following the frame's creation.
-- @usage header:InitializeConfigFunction(frame)
function GroupHeader:InitialConfigFunction(frame)
	if not frame then
		-- Cataclysm, the frame is not passed into us but the new
		-- GroupHeader does set the 1..n array slots to the frames.
		-- Since this function is only called on the creation of a new
		-- frame the newest frame will always be the last array slot
		frame = self[#self]
	else
		-- pre Cataclysm
		self[#self+1] = frame
	end
	frame.header = self
	frame.is_singleton = false
	frame.classification = self.name
	frame.classification_db = self.group_db
	frame.is_wacky = self.is_wacky

	local layout = self.group_db.layout
	frame.layout = layout

	PitBull4:ConvertIntoUnitFrame(frame)

	if frame:CanChangeAttribute() then
		if self.unitsuffix then
			frame:ProxySetAttribute("unitsuffix", self.unitsuffix)
		end

		local layout_db = PitBull4.db.profile.layouts[layout]
		frame.layout_db = layout_db

		frame:ProxySetAttribute("initial-width", layout_db.size_x * self.group_db.size_x)
		frame:ProxySetAttribute("initial-height", layout_db.size_y * self.group_db.size_y)
		frame:ProxySetAttribute("initial-unitWatch", true)
	end

	frame:_RefreshLayout() -- Normally protected by an OutOfCombatWrapper
end

--- Force unit frames to be created on the group header, even if those units don't exist.
-- Note: this is a hack to get around a Blizzard bug preventing frames from being initialized properly while in combat.
-- @usage header:ForceUnitFrameCreation()
function GroupHeader:ForceUnitFrameCreation()
	local num = self:GetMaxUnits()
	local rehide = false
	local maxColumns = self:GetAttribute("maxColumns")
	local unitsPerColumn = self:GetAttribute("unitsPerColumn")
	local startingIndex = self:GetAttribute("startingIndex")

	if unitsPerColumn and num > unitsPerColumn then
		self:ProxySetAttribute("maxColumns", num / unitsPerColumn)
	else
		self:ProxySetAttribute("maxColumns", 1)
		self:ProxySetAttribute("unitsPerColumn", num)
	end
	if not self:IsShown() then
		self:Show()
		rehide = true
	end
	self:SetAttribute("startingIndex", -num + 1) -- Not proxied to ensure an Update happens

	self:ProxySetAttribute("maxColumns", maxColumns)
	self:ProxySetAttribute("unitsPerColumn", unitsPerColumn)
	self:SetAttribute("startingIndex", startingIndex) -- Not proxied to ensure an Update happens

	if rehide then
		self:Hide()
	end

	-- this is done because the previous hack can mess up some unit references
	for _, frame in self:IterateMembers() do
		local unit = SecureButton_GetModifiedUnit(frame, "LeftButton")
		if unit ~= frame.unit then
			frame.unit = unit
			frame:Update()
		end
	end
end
GroupHeader.ForceUnitFrameCreation = PitBull4:OutOfCombatWrapper(GroupHeader.ForceUnitFrameCreation)

local function hook_SecureGroupHeader_Update()
	hook_SecureGroupHeader_Update = nil
	local function hook(self)
		if not PitBull4.all_headers[self] then
			return
		end
		if not self.force_show then
			return
		end
		if in_force_show then
			return
		end
		self:AssignFakeUnitIDs()
		PitBull4:RecheckConfigMode()
	end
	hooksecurefunc("SecureGroupHeader_Update", hook)
	hooksecurefunc("SecureGroupPetHeader_Update", hook)
end

-- utility function for AssignFakeUnitIDs
local function fill_table(tbl, ...)
	wipe(tbl)
	for i = 1, select('#', ...), 1 do
		local key = select(i, ...)
		key = tonumber(key) or key
		tbl[key] = true
	end
end

-- utility function for AssignFakeUnitIDs
local function double_fill_table(tbl, ...)
	fill_table(tbl, ...)
	for i = 1, select('#', ...), 1 do
		tbl[i] = select(i, ...)
	end
end

-- utility function for AssignFakeUnitIDs, it doctors
-- up some data so don't reuse this elsewhere
local function get_group_roster_info(super_unit_group, index, sort_dir, group_by)
	local unit, name, subgroup, class_name, role
	if super_unit_group == "raid" then
		unit = "raid"..index
		name, _, subgroup, _, _, class_name, _, _, _, role = GetRaidRosterInfo(index)
	else
		if index > 0 then
			unit = "party"..index
		else
			unit = "player"
		end
		if UnitExists(unit) then
			name = UnitName(unit)
			_, class_name = UnitClass(unit)
			-- The UnitInParty and UnitInRaid checks are an ugly workaround for thee
			-- You are not in a party bug that Blizzard created.
			if not PitBull4.leaving_world and (UnitInParty(unit) or UnitInRaid(unit)) then
				if GetPartyAssignment("MAINTANK", unit) then
					role = "MAINTANK"
				elseif  GetPartyAssignment("MAINASSIST", unit) then
					role = "MAINASSIST"
				end
			end
			subgroup = 1
		end
	end

	-- return some bogus data to get our fake unit ids to sort where we want.
	if not name then
		name = (sort_dir == "DESC" and "!" or "~")..string.format("%02d",index)
		subgroup = '!'
		class_name = '!'
	end

	return unit, name, subgroup, class_name, role
end

-- AssigneFakeUnitIDs generates a bunch of fake unit ids for
-- frames being show in config mode.  It's largely a rework
-- of SecureGroupHeader_Update for our purposes.  We need
-- to generate unit ids in roughly the same order that the
-- group header would for real frames but we want the fake
-- units to always be after the real units.  Sadly that makes
-- this code pretty downright ugly.

local sorting_table = {}
local token_table = {}
local grouping_table = {}
local temp_table = {}
function GroupHeader:AssignFakeUnitIDs()
	if not self.force_show then
		return
	end

	wipe(sorting_table)

	local super_unit_group = self.super_unit_group
	local config_mode = PitBull4.config_mode
	local start, finish, step = 1, self:GetMaxUnits(), 1

	if self.include_player then
		-- start at 0 for the player
		start = 0
		finish = finish - 1 -- GetMaxUnits already accounts for include_player
	end

	-- Limit the number of frames to the config mode for raid
	if config_mode and config_mode:sub(1,4) == "raid" and super_unit_group == "raid" then
		if config_mode == "raid" then
			if finish > 5 then
				finish = 5
			end
		elseif config_mode:sub(1,4) == "raid" then
			local num = config_mode:sub(5)+0 -- raid10, raid25, raid40 => 10, 25, 40
			if num < finish then
				finish = num
			end
		end
	end

	local name_list = self:GetAttribute("nameList")
	local group_filter = self:GetAttribute("groupFilter")
	local sort_method = self:GetAttribute("sortMethod")
	local group_by = self:GetAttribute("groupBy")
	local sort_dir = self:GetAttribute("sortDir")

	if not group_filter and not name_list then
		group_filter = "1,2,3,4,5,6,7,8"
	end


	if group_filter then
		-- Add in our bogus group to the appropriate
		-- place on the group_filter.
		if sort_dir == 'DESC' then
			group_filter = "!,"..group_filter
		else
			group_filter = group_filter..",!"
		end

		-- filter by a list of group numbers and/or classes
		fill_table(token_table, strsplit(",", group_filter))
		local strict_filter = self:GetAttribute("strictFiltering")

		for i = start, finish, 1 do
			local unit, name, subgroup, class_name, role = get_group_roster_info(super_unit_group, i, sort_dir, group_by)

			if name and (not strict_filtering
				and (token_table[subgroup] or token_table[class_name] or (role and token_table[role]))) -- non-strict filtering
				or (token_table[subgroup] and token_table[class_name]) -- strict filtering
				then
				sorting_table[#sorting_table+1] = name
				sorting_table[name] = unit
				if group_by == "GROUP" then
					grouping_table[name] = subgroup
				elseif group_by == "CLASS" then
					grouping_table[name] = class_name
				elseif group_by == "ROLE" then
					grouping_table[name] = role
				end
			end
		end

		if group_by then
			local grouping_order = self:GetAttribute("groupingOrder")

			-- Add in our bogus group token onto the grouping_order
			-- in the right place to achieve the sorting we want
			if sort_dir == 'DESC' then
				grouping_order = "!,"..grouping_order
			else
				grouping_order = grouping_order..',!'
			end

			double_fill_table(token_table, strsplit(",", grouping_order))
			wipe(temp_table)
			for _, grouping in ipairs(token_table) do
				grouping = tonumber(grouping) or grouping
				for k in ipairs(grouping_table) do
					grouping_table[k] = nil
				end
				for index, name in ipairs(sorting_table) do
					if grouping_table[name] == grouping then
						grouping_table[#grouping_table+1] = name
						temp_table[name] = true
					end
				end
				if sort_method == "NAME" then -- sort by ID by default
					sort(grouping_table)
				end
				for _, name in ipairs(grouping_table) do
					temp_table[#temp_table+1] = name
				end
			end
			-- hande units whose group didn't appear in groupingOrder
			wipe(grouping_table)
			for index, name in ipairs(sorting_table) do
				if not temp_table[name] then
					grouping_table[#grouping_table+1] = name
				end
			end
			if sort_method == "NAME" then -- sort by ID by default
				sort(grouping_table)
			end
			for _, name in ipairs(grouping_table) do
				temp_table[#temp_table+1] = name
			end

			-- copy the names back to sorting_table
			for index, name in ipairs(temp_table) do
				sorting_table[index] = name
			end
		elseif sort_method == "NAME" then -- sort by ID by default
			sort(sorting_table)
		else
			-- Have to do some reordering on ID DESC sort order
			-- since normally the fake ids would come first.
			wipe(temp_table)
			-- add in the fake units first so they end up at the end
			for _, name in ipairs(sorting_table) do
				if name:sub(1,1) == "!" then
					temp_table[#temp_table+1] = name
				end
			end
			-- now the real units
			for _, name in ipairs(sorting_table) do
				if name:sub(1,1) ~= "!" then
					temp_table[#temp_table+1] = name
				end
			end
			-- copy back to sorting_table
			for index, name in ipairs(temp_table) do
				sorting_table[index] = name
			end
		end
	else
		-- filtering via a list of names
		double_fill_table(sorting_table, strsplit(",", name_list))
		for i = start, finish, 1 do
			local unit, name = get_group_roster_info(super_unit_group, i)
			if sorting_table[name] then
				sorting_table[name] = unit
			end
		end
		for i = #sorting_table, 1, -1 do
			local name = sorting_table[i]
			if sorting_table[name] == true then
				tremove(sorting_table, i)
			end
		end
		if sort_method == "NAME" then
			sort(sorting_table)
		end
	end

	-- setup to actually set the units on the frames.
	-- From here on out the code is roughly borrowed
	-- from configureChildren.  However, we shortcut
	-- startingIndex to always be 1.  If we ever
	-- configure the startingIndex to be something else
	-- this code will have to be adjusted.
	start, finish = 1, #sorting_table
	if sort_dir == "DESC" then
		start, finish, step = finish, start, -1
	end

	local frame_num = 0
	for i = start, finish, step do
		frame_num = frame_num + 1
		local frame = self[frame_num]

		if not frame.guid then
			local old_unit = frame:GetAttribute("unit")
			local unit = sorting_table[sorting_table[i]]
			frame:SetAttribute("unit", unit)
			if old_unit ~= unit then
				frame:Update()
			end
		elseif DEBUG then
			-- Spit out errors to chat if our code didn't
			-- come up with the same unit ids for the real frames
			-- that the group header did.
			local unit = frame:GetAttribute("unit")
			local expected_unit = sorting_table[sorting_table[i]]
			if unit ~= expected_unit then
				print("PitBull4 expected "..tostring(expected_unit).." but found "..tostring(unit).." for "..frame:GetName())
			end
		end
	end
end
GroupHeader.AssignFakeUnitIDs = PitBull4:OutOfCombatWrapper(GroupHeader.AssignFakeUnitIDs)

local ipairs_upto_num
do
	local ipairs_helpers = setmetatable({}, {__index=function(self, num)
		local f = function(t, i)
			i = i + 1
			if i > num then
				return nil
			end

			local v = t[i]
			if v == nil then
				return nil
			end

			return i, v
		end
		self[num] = f
		return f
	end})
	function ipairs_upto_num(t, num)
		return ipairs_helpers[num], t, 0
	end
end

local function get_filter_type_count(...)
	local start = select(1, ...)
	return tonumber(start) and true or false, select('#', ...)
end

function GroupHeader:GetMaxUnits()
	if self.super_unit_group == "raid" then
		if self.group_db then
			local group_filter = self.group_db.group_filter
			if group_filter then
				if group_filter == "" then
				-- Everything filtered, but always have at least one unit
					return 1
				end

				-- If we're filtering by raid group we may not need all 40
				-- units for this group header.
				local by_raid_group,count = get_filter_type_count(strsplit(",",group_filter))
				if by_raid_group then
					return MEMBERS_PER_RAID_GROUP * count
				end
			end
		end

		-- Everything else we're gonna have to go by max.
		return MAX_RAID_MEMBERS
	else
		if self.include_player then
			return MAX_PARTY_MEMBERS_WITH_PLAYER
		else
			return MAX_PARTY_MEMBERS
		end
	end
end

local make_set
do
	local set = {}
	function make_set(...)
		wipe(set)
		local n = select('#', ...)
		for i = 1, n do
			set[select(i, ...)] = true
		end

		return set, n
	end
end

function GroupHeader:IterateMembers(guess_num)
	local max_units = self:GetMaxUnits()
	local num
	if guess_num then
		local config_mode = PitBull4.config_mode
		if config_mode == "solo" then
			num = self.include_player and 1 or 0
		elseif config_mode == "party" then
			num = self.include_player and MAX_PARTY_MEMBERS_WITH_PLAYER or MAX_PARTY_MEMBERS
		elseif config_mode then
			if config_mode == "raid" then
				num = 5
			else
				num = config_mode:sub(5)+0 -- raid10, raid25, raid40 => 10, 25, 40
			end
			-- check filters

			local filter = self.group_filter
			if not filter then
				-- do nothing, all is shown
			elseif filter == "" then
				-- all is hidden for some reason
				num = 0
			else
				local set, count = make_set((","):split(filter))
				local start = next(set)
				if start == "MAINTANK" or start == "MAINASSIST" then
					num = MINIMUM_EXAMPLE_GROUP
				elseif RAID_CLASS_COLORS[start] then
					num = math.ceil(num * count / NUM_CLASSES)
					if num < MINIMUM_EXAMPLE_GROUP then
						num = MINIMUM_EXAMPLE_GROUP
					end
				elseif tonumber(start) then
					local count = 0
					for i = 1, num / MEMBERS_PER_RAID_GROUP do
						if set[i..""] then
							count = count + 1
						end
					end
					num = count * MEMBERS_PER_RAID_GROUP
				end
			end
		end
	end

	if not num or num > max_units then
		num = max_units
	end
	return ipairs_upto_num(self, num)
end

function GroupHeader:ForceShow()
	in_force_show = true
	if not self.force_show then
		if hook_SecureGroupHeader_Update then
			hook_SecureGroupHeader_Update()
		end
		self.force_show = true
		self:ForceUnitFrameCreation()
		self:AssignFakeUnitIDs()
		if not self.label then
			local label = self:CreateFontString(self:GetName() .. "_Label", "OVERLAY", "ChatFontNormal")
			self.label = label
			local font, size, modifier = label:GetFont()
			label:SetFont(font, size * 1.5, modifier)
			label:SetText(self.name)
			position_label(self, label)
		end
		self.label:Show()
	end

	-- Always make sure that the members ForceShow() is called
	for _, frame in self:IterateMembers(true) do
		frame:ForceShow()
		frame:Update(true, true)
	end
	in_force_show = false
end
GroupHeader.ForceShow = PitBull4:OutOfCombatWrapper(GroupHeader.ForceShow)

function GroupHeader:UnforceShow()
	if not self.force_show then
		return
	end
	self.force_show = nil
	self.label:Hide()
	for _, frame in ipairs(self) do
		frame:UnforceShow()
	end
end
GroupHeader.UnforceShow = PitBull4:OutOfCombatWrapper(GroupHeader.UnforceShow)

function GroupHeader:Rename(name)
	if self.name == name then
		return
	end

	local use_pet_header = self.group_db.use_pet_header
	local prefix = use_pet_header and "PitBull4_PetGroups_" or "PitBull4_Groups_"

	local old_header_name = prefix .. self.name
	local new_header_name = prefix .. name

	PitBull4.name_to_header[self.name] = nil
	PitBull4.name_to_header[name] = self
	_G[old_header_name] = nil
	_G[new_header_name] = self
	self.name = name
	if self.label then
		self.label:SetText(name)
	end

	for i, frame in ipairs(self) do
		frame.classification = name
	end
end

function GroupHeader:ClearFrames()
	-- Clears the frames over a 10 minute period.  Starting from the
	-- end working our way to the front
	local clear_index = self.clear_index
	-- Frames will have no guid at this point so Update == Clear
	self[clear_index]:Update()
	clear_index = clear_index - 1
	if clear_index > 0 then
		local max_units = self:GetMaxUnits()
		if clear_index > max_units then
			max_units = clear_index + 1
		end
		local delay = 600 / (max_units - 1)
		self.clear_index = clear_index
		self.clear_timer = PitBull4:ScheduleTimer(self.ClearFrames, delay, self)
	else
		self.clear_index = nil
		self.clear_timer = nil
	end
end

function GroupHeader__scripts:OnHide()
	if self.dont_update then return end
	-- Remove any existing timer so we don't just grow timers endlessly.
	local clear_timer = self.clear_timer
	if clear_timer then
		PitBull4:CancelTimer(clear_timer)
	end
	-- Start clearing the frames in 5 minutes.
	self.clear_index = #self
	self.clear_timer = PitBull4:ScheduleTimer(self.ClearFrames, 300, self)
end

function GroupHeader__scripts:OnShow()
	if self.dont_update then return end
	local clear_timer = self.clear_timer
	if clear_timer then
		PitBull4:CancelTimer(clear_timer, true)
		self.clear_timer = nil
		self.clear_index = nil
	end
end

local moving_frame = nil
function MemberUnitFrame__scripts:OnDragStart()
	local db = PitBull4.db.profile
	if db.lock_movement or InCombatLockdown() then
		return
	end

	local header = self.header
	moving_frame = header

	if db.frame_snap then
		LibStub("LibSimpleSticky-1.0"):StartMoving(header, PitBull4.all_frames_list, 0, 0, 0, 0)
	else
		header:StartMoving()
	end
end

function MemberUnitFrame__scripts:OnDragStop()
	local header = self.header
	if moving_frame ~= header then return end
	moving_frame = nil

	if PitBull4.db.profile.frame_snap then
		LibStub("LibSimpleSticky-1.0"):StopMoving(header)
	else
		header:StopMovingOrSizing()
	end

	local ui_scale = UIParent:GetEffectiveScale()
	local scale = header[1]:GetEffectiveScale() / ui_scale

	local x, y = header[1]:GetCenter()
	x, y = x * scale, y * scale

	x = x - GetScreenWidth()/2
	y = y - GetScreenHeight()/2

	header.group_db.position_x = x
	header.group_db.position_y = y

	LibStub("AceConfigRegistry-3.0"):NotifyChange("PitBull4")

	header:RefreshLayout(true)
end

function MemberUnitFrame__scripts:OnMouseUp(button)
	if button == "LeftButton" then
		return MemberUnitFrame__scripts.OnDragStop(self)
	end
end

function MemberUnitFrame:PLAYER_REGEN_DISABLED()
	if moving_frame then
		MemberUnitFrame__scripts.OnDragStop(moving_frame[1])
	end
end

local clickcast_register = [[
  local button = self:GetFrameRef("pb4_temp")
  local clickcast_header = self:GetFrameRef("clickcast_header")
  if clickcast_header:GetAttribute("clickcast_register") then
    clickcast_header:SetAttribute("clickcast_button",button)
    clickcast_header:RunAttribute("clickcast_register")
  end
]]

local clickcast_unregister = [[
  local button = self:GetFrameRef("pb4_temp")
  local clickcast_header = self:GetFrameRef("clickcast_header")
  if clickcast_header:GetAttribute("clickcast_unregister") then
    clickcast_header:SetAttribute("clickcast_button",button)
    clickcast_header:RunAttribute("clickcast_unregister")
  end
]]

-- Set the frame as able to be clicked through or not.
-- @usage frame:SetClickThroughState(true)
function MemberUnitFrame:SetClickThroughState(state)
	local mouse_state = not not self:IsMouseEnabled()
	if not state ~= mouse_state then
		if ClickCastHeader then
			local header = self:GetParent()
			header:SetFrameRef("pb4_temp",self)
			header:Execute(not mouse_state and clickcast_register or clickcast_unregister)
		end
		self:EnableMouse(not mouse_state)
	end
end
MemberUnitFrame.SetClickThroughState = PitBull4:OutOfCombatWrapper(MemberUnitFrame.SetClickThroughState)

--- Reset the size of the unit frame, not position as that is handled through the group header.
-- @usage frame:RefixSizeAndPosition()
function MemberUnitFrame:RefixSizeAndPosition()
	if not self:CanChangeProtectedState() then return end
	local layout_db = self.layout_db
	local classification_db = self.classification_db

	self:SetWidth(layout_db.size_x * classification_db.size_x)
	self:SetHeight(layout_db.size_y * classification_db.size_y)
end

--- Add the proper functions and scripts to a SecureGroupHeaderTemplate or SecureGroupPetHeaderTemplate, as well as some initialization.
-- @param frame a Frame which inherits from SecureGroupHeaderTemplate or SecureGroupPetHeaderTemplate
-- @usage PitBull4:ConvertIntoGroupHeader(header)
function PitBull4:ConvertIntoGroupHeader(header)
	if DEBUG then
		expect(header, 'typeof', 'frame')
		expect(header, 'frametype', 'Frame')
	end

	-- Stop the group header from listening to UNIT_NAME_UPDATE.
	-- Allowing it to do so is a huge performance drain since the
	-- GroupHeader's OnEvent updates the header regardless of the unit
	-- passed in the argument.  Many UNIT_NAME_UPDATE events can be
	-- generated when zoning into battlegrounds, spirit rezes in
	-- battlegrounds, pet rezes, etc.  This should prevent some
	-- stuttering isseus with BGs.  See this post for more details:
	-- http://forums.wowace.com/showthread.php?p=111494#post111494
	self:UnregisterEvent("UNIT_NAME_UPDATE")

	self.all_headers[header] = true
	self.name_to_header[header.name] = header

	for k, v in pairs(GroupHeader__scripts) do
		header:HookScript(k, v)
	end

	for k, v in pairs(GroupHeader) do
		header[k] = v
	end

	if ClickCastHeader then
		SecureHandler_OnLoad(header)
		header:SetFrameRef("clickcast_header", ClickCastHeader)
	end

	-- this is done to pass self in properly
	function header.initialConfigFunction(...)
		return header:InitialConfigFunction(...)
	end

	header:SetAttribute("initialConfigFunction",
	[[
    local header = self:GetParent()
    local unitsuffix = header:GetAttribute("unitsuffix")
    if unitsuffix then
      self:SetAttribute("unitsuffix",unitsuffix)
    end
    self:SetWidth(header:GetAttribute("unitWidth"))
    self:SetHeight(header:GetAttribute("unitHeight"))
    RegisterUnitWatch(self)
    self:SetAttribute("*type1", "target")
    self:SetAttribute("*type2", "menu")
    local click_through = header:GetAttribute("clickThrough")
    if not click_through then
      -- Verify important the CallMethod is done BEFORE the frame is
      -- registered with Clique so that Clique can override our click
      -- registrations.
      header:CallMethod("InitialConfigFunction")
      -- Support for Clique
      local clickcast_header = header:GetFrameRef("clickcast_header")
      if clickcast_header then
        clickcast_header:SetAttribute("clickcast_button", self)
        clickcast_header:RunAttribute("clickcast_register")
      end
    else
      self:EnableMouse(false)
      -- Very important that the CallMethod is done AFTER the mouse is
      -- potentially disabled above becuase otherwise it will create a
      -- stack overflow.
      header:CallMethod("InitialConfigFunction")
    end
  ]])

	header:RefreshGroup(true)

	header:SetMovable(true)
end