-- ------------------------------------------------------------------------------ --
--                                TradeSkillMaster                                --
--                          https://tradeskillmaster.com                          --
--    All Rights Reserved - Detailed license information included with addon.     --
-- ------------------------------------------------------------------------------ --

local _, TSM = ...
local L = TSM.Include("Locale").GetTable()
local TableRow = TSM.Include("LibTSMClass").DefineClass("TableRow")
local Table = TSM.Include("Util.Table")
local Math = TSM.Include("Util.Math")
local Wow = TSM.Include("Util.Wow")
local Theme = TSM.Include("Util.Theme")
local ScriptWrapper = TSM.Include("Util.ScriptWrapper")
local ItemInfo = TSM.Include("Service.ItemInfo")
local Tooltip = TSM.Include("UI.Tooltip")
TSM.UI.Util.TableRow = TableRow
local private = {
	rowFrameLookup = {},
	layoutTemp = {},
	colIdsTemp = {},
}
local FLAG_WIDTH = 6
local FLAG_SPACING = 2
local ICON_SPACING = 4
local MIN_TEXT_WIDTH = 12
local RESIZER_WIDTH = 4
local INDENT_WIDTH = 8
local HEADER_LINE_WIDTH = 2
local MORE_COL_WIDTH = 8
local FULL_TEXT_TOOLTIP_DELAY_S = 1



-- ============================================================================
-- TableRow - Public Class Methods
-- ============================================================================

function TableRow.__init(self)
	self._scrollingTable = nil
	self._tableInfo = nil
	self._rowData = nil
	self._texts = {}
	self._icons = {}
	self._rotatingIcons = {}
	self._buttons = {}
	self._recycled = {
		buttons = {},
		texts = {},
		icons = {},
		rotatingIcons = {},
	}
	self._sortId = nil
	self._sortAscending = true
	self._state = nil

	local frame = CreateFrame("Button", nil, nil, nil)
	frame:RegisterForClicks("LeftButtonUp", "RightButtonUp")
	self._frame = frame
	private.rowFrameLookup[frame] = self

	frame.background = frame:CreateTexture(nil, "BACKGROUND")
	frame.background:SetAllPoints()

	frame.highlight = frame:CreateTexture(nil, "ARTWORK", -1)
	frame.highlight:SetAllPoints()
	frame.highlight:Hide()

	frame.sortBackground = frame:CreateTexture(nil, "ARTWORK", -2)
	frame.sortBackground:Hide()

	frame.sortFlag = frame:CreateTexture(nil, "ARTWORK", -1)
	frame.sortFlag:SetHeight(3)
	frame.sortFlag:Hide()
end

function TableRow.Acquire(self, scrollingTable, isHeader)
	self._scrollingTable = scrollingTable
	self._tableInfo = self._scrollingTable._tableInfo

	self._frame:SetParent(isHeader and self._scrollingTable._hContent or self._scrollingTable._content)
	self._frame:SetHitRectInsets(0, 0, 0, 0)
	self._frame:Show()
	self._frame.highlight:Hide()
	self._frame.sortBackground:Hide()
	self._frame.sortFlag:Hide()

	if isHeader then
		self:_CreateHeaderRowCols()
		self._frame:SetPoint("TOPLEFT", 0, -HEADER_LINE_WIDTH)
		self._frame:SetPoint("TOPRIGHT", 0, -HEADER_LINE_WIDTH)
		self:_LayoutHeaderRow()
	else
		self:_CreateDataRowCols()
		ScriptWrapper.Set(self._frame, "OnEnter", private.RowOnEnter, self)
		ScriptWrapper.Set(self._frame, "OnLeave", private.RowOnLeave, self)
		ScriptWrapper.Set(self._frame, "OnClick", private.RowOnClick, self)
		if scrollingTable._rightClickToggle then
			ScriptWrapper.Set(self._frame, "OnMouseDown", private.RowOnMouseDown, self)
		else
			ScriptWrapper.Clear(self._frame, "OnMouseDown")
		end
		self:_LayoutDataRow()
	end
end

function TableRow.Release(self)
	self._frame:Hide()
	for _, text in pairs(self._texts) do
		text:Hide()
		text:ClearAllPoints()
		text:SetWidth(0)
		text:SetHeight(0)
		text:SetTextColor(1, 1, 1, 1)
		tinsert(self._recycled.texts, text)
	end
	wipe(self._texts)
	for _, icon in pairs(self._icons) do
		icon:Hide()
		icon:SetDrawLayer("ARTWORK", 0)
		icon:SetTexture(nil)
		icon:SetTexCoord(0, 0, 0, 1, 1, 0, 1, 1)
		icon:SetColorTexture(0, 0, 0, 0)
		icon:SetVertexColor(1, 1, 1, 1)
		icon:ClearAllPoints()
		icon:SetWidth(0)
		icon:SetHeight(0)
		tinsert(self._recycled.icons, icon)
	end
	wipe(self._icons)
	for _, icon in pairs(self._rotatingIcons) do
		icon.ag:Stop()
		icon:Hide()
		icon:SetDrawLayer("ARTWORK", 0)
		icon:SetTexture(nil)
		icon:SetTexCoord(0, 0, 0, 1, 1, 0, 1, 1)
		icon:SetColorTexture(0, 0, 0, 0)
		icon:SetVertexColor(1, 1, 1, 1)
		icon:ClearAllPoints()
		icon:SetWidth(0)
		icon:SetHeight(0)
		tinsert(self._recycled.rotatingIcons, icon)
	end
	wipe(self._rotatingIcons)
	for _, button in pairs(self._buttons) do
		if button.isShowingTooltip then
			Tooltip.Hide()
			button.isShowingTooltip = nil
		end
		button:Hide()
		button:SetMouseClickEnabled(true)
		button:RegisterForDrag(nil)
		button:SetResizable(false)
		button:SetMovable(false)
		ScriptWrapper.Clear(button, "OnEnter")
		ScriptWrapper.Clear(button, "OnLeave")
		ScriptWrapper.Clear(button, "OnClick")
		ScriptWrapper.Clear(button, "OnMouseDown")
		ScriptWrapper.Clear(button, "OnMouseUp")
		ScriptWrapper.Clear(button, "OnUpdate")
		button:SetParent(nil)
		button:ClearAllPoints()
		button:SetWidth(0)
		button:SetHeight(0)
		tinsert(self._recycled.buttons, button)
	end
	wipe(self._buttons)

	self._scrollingTable = nil
	self._tableInfo = nil
	self._rowData = nil
	self._state = nil
	self._frame:SetParent(nil)
	self._frame:ClearAllPoints()
	ScriptWrapper.Clear(self._frame, "OnEnter")
	ScriptWrapper.Clear(self._frame, "OnLeave")
	ScriptWrapper.Clear(self._frame, "OnClick")
end

function TableRow.SetData(self, data)
	for _, col in self._tableInfo:_ColIterator() do
		local id = col:_GetId()
		self._texts[id]:SetText(col:_GetText(data))
		if col:_GetIconSize() then
			local button = self._buttons["_icon_"..id]
			local texture = col:_GetIcon(data, button and button:IsMouseOver())
			if type(texture) == "string" and TSM.UI.TexturePacks.IsValid(texture) then
				TSM.UI.TexturePacks.SetTexture(self._icons[id], texture)
			else
				self._icons[id]:SetTexture(texture)
			end
		end
	end
	self._rowData = data
	self:_LayoutDataRow()
end

function TableRow.SetHeaderData(self)
	if self._scrollingTable._headerHidden then
		return
	end
	for _, col in self._tableInfo:_ColIterator() do
		if not col:_GetTitleIcon() then
			self._texts[col:_GetId()]:SetText(col:_GetTitle())
		end
	end
end

function TableRow.GetData(self)
	return self._rowData
end

function TableRow.ClearData(self)
	self._rowData = nil
end

function TableRow.SetHeight(self, height)
	for _, text in pairs(self._texts) do
		text:SetHeight(height)
	end
	for _, btn in pairs(self._buttons) do
		btn:SetHeight(height)
	end
	for _, col in ipairs(self._tableInfo:_GetCols()) do
		local id = col:_GetId()
		local flag = self._icons["_flag_"..id]
		if flag then
			flag:SetHeight(height - FLAG_SPACING * 2)
		end
	end
	self._frame:SetHeight(height)
end

function TableRow.SetBackgroundColor(self, color)
	self._frame.background:SetColorTexture(color:GetFractionalRGBA())
end

function TableRow.SetTextColor(self, color)
	for _, col in ipairs(self._tableInfo:_GetCols()) do
		local id = col:_GetId()
		local text = self._texts[id]
		if text then
			text:SetTextColor(color:GetFractionalRGBA())
		end
	end
end

function TableRow.SetVisible(self, visible)
	if visible == self._frame:IsVisible() then
		return
	end
	if visible then
		self._frame:Show()
		self._frame.highlight:Hide()
	else
		self._frame:Hide()
	end
end

function TableRow.IsVisible(self)
	return self._frame:IsVisible()
end

function TableRow.SetHighlightState(self, state, noLayout)
	self._state = state
	local highlightTint, shouldLayoutActionIcons = nil, false
	if state == "selectedHover" then
		highlightTint = "+SELECTED_HOVER"
	elseif state == "selected" then
		highlightTint = "+SELECTED"
	elseif state == "hover" then
		highlightTint = self._scrollingTable._rowHoverEnabled and "+HOVER" or nil
		shouldLayoutActionIcons = true
	elseif state == nil then
		shouldLayoutActionIcons = true
	else
		error("Invalid state: "..state)
	end
	if highlightTint then
		local backgroundColor = self._scrollingTable._backgroundColor
		self._frame.highlight:SetColorTexture(Theme.GetColor(backgroundColor):GetTint(highlightTint):GetFractionalRGBA())
		self._frame.highlight:Show()
	else
		self._frame.highlight:Hide()
	end
	if not noLayout then
		local shouldLayout = false
		for _, col in self._tableInfo:_VisibleColIterator() do
			if col:_HasFlag() or (shouldLayoutActionIcons and select(3, col:_GetActionIconInfo())) then
				shouldLayout = true
				break
			end
		end
		if shouldLayout then
			self:_LayoutDataRow()
		end
	end
end

function TableRow.IsMouseOver(self)
	return self._frame:IsMouseOver()
end

function TableRow.SetHitRectInsets(self, left, right, top, bottom)
	for _, tooltipFrame in pairs(self._buttons) do
		tooltipFrame:SetHitRectInsets(left, right, top, bottom)
	end
	self._frame:SetHitRectInsets(left, right, top, bottom)
end

function TableRow.SetSort(self, sortId, sortAscending)
	if sortId == self._sortId and sortAscending == self._sortAscending then
		return
	end
	self._sortId = sortId
	self._sortAscending = sortAscending
	self:_LayoutHeaderRow()
end



-- ============================================================================
-- TableRow - Private Class Methods
-- ============================================================================

function TableRow._GetFontString(self)
	local fontString = tremove(self._recycled.texts)
	if not fontString then
		fontString = self._frame:CreateFontString()
		fontString:SetShadowColor(0, 0, 0, 0)
		fontString:SetWordWrap(false)
	end
	fontString:Show()
	return fontString
end

function TableRow._GetTexture(self)
	local texture = tremove(self._recycled.icons)
	if not texture then
		texture = self._frame:CreateTexture()
	end
	texture:Show()
	return texture
end

function TableRow._GetRotatingTexture(self)
	local texture = tremove(self._recycled.rotatingIcons)
	if not texture then
		texture = self._frame:CreateTexture()
		texture.ag = texture:CreateAnimationGroup()
		local spin = texture.ag:CreateAnimation("Rotation")
		spin:SetDuration(2)
		spin:SetDegrees(360)
		texture.ag:SetLooping("REPEAT")
	end
	texture:Show()
	return texture
end

function TableRow._GetButton(self)
	local button = tremove(self._recycled.buttons)
	if not button then
		button = CreateFrame("Button", nil, self._frame, nil)
	end
	button:SetParent(self._frame)
	button:SetHitRectInsets(0, 0, 0, 0)
	button:RegisterForClicks("LeftButtonUp", "RightButtonUp")
	button:Show()
	return button
end

function TableRow._GetSepTexture(self)
	local sepTexture = self:_GetTexture()
	sepTexture:SetDrawLayer("BORDER")
	sepTexture:SetWidth(HEADER_LINE_WIDTH)
	return sepTexture
end

function TableRow._CreateHeaderRowCols(self)
	if self._scrollingTable._headerHidden then
		return
	end
	if self._scrollingTable:_CanResizeCols() then
		-- craete the "more" column
		local button = self:_GetButton()
		ScriptWrapper.Set(button, "OnClick", private.MoreColOnClick, self)
		self._buttons._more = button
		local moreIcon = self:_GetTexture()
		moreIcon:SetDrawLayer("ARTWORK")
		TSM.UI.TexturePacks.SetTextureAndSize(moreIcon, "iconPack.12x12/More/Vertical")
		self._icons._more = moreIcon
		self._icons._sep__more = self:_GetSepTexture()
	end

	for _, col in self._tableInfo:_ColIterator() do
		local id = col:_GetId()
		local button = self:_GetButton()
		ScriptWrapper.Set(button, "OnClick", private.HeaderColOnClick)
		ScriptWrapper.Set(button, "OnEnter", private.HeaderColOnEnter)
		ScriptWrapper.Set(button, "OnLeave", private.HeaderColOnLeave)
		button:SetResizable(true)
		self._buttons[id] = button
		if self._scrollingTable:_CanResizeCols() then
			local resizerButton = self:_GetButton()
			resizerButton:SetMovable(true)
			ScriptWrapper.Set(resizerButton, "OnEnter", private.ResizerOnEnter)
			ScriptWrapper.Set(resizerButton, "OnLeave", private.ResizerOnLeave)
			ScriptWrapper.Set(resizerButton, "OnMouseDown", private.ResizerOnMouseDown)
			ScriptWrapper.Set(resizerButton, "OnMouseUp", private.ResizerOnMouseUp)
			ScriptWrapper.Set(resizerButton, "OnClick", private.ResizerOnClick)
			self._buttons["_resizer_"..id] = resizerButton
			local resizerHighlight = self:_GetTexture()
			resizerHighlight:SetDrawLayer("ARTWORK")
			resizerHighlight:SetPoint("TOPLEFT", resizerButton)
			resizerHighlight:SetPoint("BOTTOMRIGHT", resizerButton)
			resizerHighlight:SetColorTexture(Theme.GetColor("ACTIVE_BG+HOVER"):GetFractionalRGBA())
			resizerHighlight:Hide()
			self._icons["_resizer_"..id] = resizerHighlight
		end
		self._icons["_sep_"..id] = self:_GetSepTexture()
		local iconTexture = col:_GetTitleIcon()
		if iconTexture then
			local icon = self:_GetTexture()
			icon:SetDrawLayer("ARTWORK", 1)
			TSM.UI.TexturePacks.SetTextureAndSize(icon, iconTexture)
			self._icons[id] = icon
		else
			local text = self:_GetFontString()
			text:SetFont(Theme.GetFont("BODY_BODY3_MEDIUM"):GetWowFont())
			text:SetJustifyH("LEFT")
			text:SetText(col:_GetTitle())
			self._texts[id] = text
		end
	end
end

function TableRow._CreateDataRowCols(self)
	for _, col in self._tableInfo:_ColIterator() do
		local id = col:_GetId()
		local iconSize = col:_GetIconSize()
		if iconSize then
			local icon = self:_GetTexture()
			icon:SetDrawLayer("ARTWORK", 1)
			icon:SetWidth(iconSize)
			icon:SetHeight(iconSize)
			self._icons[id] = icon

			if col:_IsIconHoverEnabled() then
				local iconBtn = self:_GetButton()
				iconBtn:SetAllPoints(icon)
				ScriptWrapper.Set(iconBtn, "OnEnter", private.IconButtonOnEnter)
				ScriptWrapper.Set(iconBtn, "OnLeave", private.IconButtonOnLeave)
				ScriptWrapper.Set(iconBtn, "OnClick", private.IconButtonOnClick)
				self._buttons["_icon_"..id] = iconBtn
			end
		end
		-- need to create a text element even if there's no text, as it's used for the layout
		local text = self:_GetFontString()
		if col:_HasText() then
			text:SetFont(col:_GetWowFont())
			text:SetJustifyH(col:_GetJustifyH())
		else
			-- use an arbitrary default font since the text will be an empty string
			text:SetFontObject(GameFontNormal)
		end
		self._texts[id] = text
		if col:_HasTooltip() then
			local tooltipFrame = self:_GetButton()
			ScriptWrapper.Set(tooltipFrame, "OnEnter", private.TooltipFrameOnEnter)
			ScriptWrapper.Set(tooltipFrame, "OnLeave", private.TooltipFrameOnLeave)
			ScriptWrapper.Set(tooltipFrame, "OnClick", private.TooltipFrameOnClick)
			if self._scrollingTable._rightClickToggle then
				ScriptWrapper.Set(tooltipFrame, "OnMouseDown", private.RowOnMouseDown, self)
			end
			self._buttons[id] = tooltipFrame
		end
		if col:_HasFlag() then
			-- add the flag texture
			local flag = self:_GetTexture()
			flag:SetDrawLayer("ARTWORK", 1)
			flag:SetWidth(FLAG_WIDTH)
			self._icons["_flag_"..id] = flag
		end
		if col:_HasExpander() then
			-- add the expander texture
			local expander = self:_GetTexture()
			expander:SetDrawLayer("ARTWORK", 1)
			self._icons["_expander_"..id] = expander

			local expanderBtn = self:_GetButton()
			expanderBtn:SetAllPoints(expander)
			ScriptWrapper.SetPropagate(expanderBtn, "OnEnter")
			ScriptWrapper.Set(expanderBtn, "OnLeave", private.ExpanderOnLeave, self)
			ScriptWrapper.Set(expanderBtn, "OnClick", private.ExpanderOnClick, self)
			self._buttons["_expander_"..id] = expanderBtn
		end
		if col:_HasCheck() then
			-- add the check texture
			local expander = self:_GetTexture()
			expander:SetDrawLayer("ARTWORK", 1)
			TSM.UI.TexturePacks.SetTextureAndSize(expander, "iconPack.14x14/Checkmark/Default")
			self._icons["_check_"..id] = expander
		end
		if col:_HasBadge() then
			local badgeText = self:_GetFontString()
			badgeText:SetFont(Theme.GetFont("TABLE_TABLE1"):GetWowFont())
			badgeText:SetTextColor(Theme.GetColor("INDICATOR"):GetFractionalRGBA())
			badgeText:SetJustifyH("RIGHT")
			badgeText:SetJustifyV("MIDDLE")
			self._texts["_badge_"..id] = badgeText
		end
		-- add the action icons
		local numActionIcons, actionIconSize = col:_GetActionIconInfo()
		for i = 1, numActionIcons do
			local icon = self:_GetRotatingTexture()
			icon:SetDrawLayer("ARTWORK", 1)
			icon:SetWidth(actionIconSize)
			icon:SetHeight(actionIconSize)
			self._rotatingIcons["_action"..i.."_"..id] = icon

			local iconBtn = self:_GetButton()
			iconBtn:SetAllPoints(icon)
			ScriptWrapper.Set(iconBtn, "OnEnter", private.ActionIconButtonOnEnter)
			ScriptWrapper.Set(iconBtn, "OnLeave", private.ActionIconButtonOnLeave)
			ScriptWrapper.Set(iconBtn, "OnClick", private.ActionIconButtonOnClick)
			self._buttons["_action"..i.."_"..id] = iconBtn
		end
	end
end

function TableRow._LayoutHeaderRow(self)
	if self._scrollingTable._headerHidden then
		return
	end

	-- hide any hidden cols
	for _, col in self._tableInfo:_HiddenColIterator() do
		local id = col:_GetId()
		local button = self._buttons[id]
		button:Hide()
		local sepIcon = self._icons["_sep_"..id]
		sepIcon:Hide()

		if self._scrollingTable:_CanResizeCols() then
			local resizerButton = self._buttons["_resizer_"..id]
			resizerButton:Hide()
		end

		local iconTexture = col:_GetTitleIcon()
		if iconTexture then
			self._icons[id]:Hide()
		else
			self._texts[id]:Hide()
		end
	end

	local cols = self._tableInfo:_GetVisibleCols()

	if self._scrollingTable:_CanResizeCols() then
		-- layout the "more" column
		local button = self._buttons._more
		button:SetPoint("LEFT", Theme.GetColSpacing() / 2, 0)
		button:SetWidth(MORE_COL_WIDTH)
		local icon = self._icons._more
		icon:SetPoint("CENTER", button)
		local sepIcon = self._icons._sep__more
		sepIcon:SetPoint("TOP", button, "TOPRIGHT", Theme.GetColSpacing() / 2, 0)
		sepIcon:SetPoint("BOTTOM", button, "BOTTOMRIGHT", Theme.GetColSpacing() / 2, 0)
		sepIcon:SetColorTexture(Theme.GetColor("ACTIVE_BG"):GetFractionalRGBA())
	end

	-- build buttons from the left until we get to the col without a width, then switch to building from the right
	for i, col, isAscending in Table.InwardIterator(cols) do
		local id = col:_GetId()
		local button = self._buttons[id]
		local leftButton = i > 1 and self._buttons[cols[i - 1]:_GetId()] or (self._scrollingTable:_CanResizeCols() and self._buttons._more) or nil
		local rightButton = i < #cols and self._buttons[cols[i + 1]:_GetId()] or nil
		button:Show()
		button:ClearAllPoints()

		if self._scrollingTable:_CanResizeCols() then
			local resizerBtn = self._buttons["_resizer_"..id]
			if self._scrollingTable:_IsColWidthLocked() then
				resizerBtn:Disable()
				resizerBtn:SetMovable(false)
				resizerBtn:SetMouseClickEnabled(false)
			else
				resizerBtn:Enable()
				resizerBtn:SetMovable(true)
				resizerBtn:SetMouseClickEnabled(true)
			end
		end

		local sepIcon = self._icons["_sep_"..id]
		sepIcon:Show()
		sepIcon:SetColorTexture(Theme.GetColor("ACTIVE_BG"):GetFractionalRGBA())
		sepIcon:SetPoint("TOP", button, "TOPRIGHT", Theme.GetColSpacing() / 2, 0)
		sepIcon:SetPoint("BOTTOM", button, "BOTTOMRIGHT", Theme.GetColSpacing() / 2, 0)

		-- layout the button
		if isAscending then
			if leftButton then
				button:SetPoint("LEFT", leftButton, "RIGHT", Theme.GetColSpacing(), 0)
			else
				button:SetPoint("LEFT", Theme.GetColSpacing() / 2, 0)
			end
		else
			if rightButton then
				button:SetPoint("RIGHT", rightButton, "LEFT", -Theme.GetColSpacing(), 0)
			else
				button:SetPoint("RIGHT", -Theme.GetColSpacing() / 2, 0)
			end
		end

		if self._scrollingTable:_CanResizeCols() then
			local iconSize = col:_GetIconSize()
			local iconTexture = col:_GetTitleIcon()
			button:SetWidth(self._scrollingTable:_GetColWidth(id))

			-- the minimum width of the content is our minimum text width plus the size of the content icon
			local minContentWidth = 0
			if col:_HasText() then
				minContentWidth = minContentWidth + MIN_TEXT_WIDTH
			end
			if iconSize then
				minContentWidth = minContentWidth + iconSize + ICON_SPACING
			end
			if col:_HasFlag() then
				minContentWidth = minContentWidth + FLAG_WIDTH
			end
			if col:_HasExpander() then
				minContentWidth = minContentWidth + TSM.UI.TexturePacks.GetWidth("iconPack.12x12/Caret/Right") + ICON_SPACING
			end
			if col:_HasCheck() then
				minContentWidth = minContentWidth + TSM.UI.TexturePacks.GetWidth("iconPack.14x14/Checkmark/Default") + ICON_SPACING
			end
			if col:_HasBadge() then
				minContentWidth = minContentWidth + TSM.UI.TexturePacks.GetWidth("uiFrames.AuctionCounterTexture") + ICON_SPACING
			end
			-- the minimum header width is either our header icon width or the minimum text width
			local minHeaderWidth = iconTexture and TSM.UI.TexturePacks.GetWidth(iconTexture) or MIN_TEXT_WIDTH
			button:SetMinResize(max(minContentWidth, minHeaderWidth), 0)

			-- layout the resizer button
			local resizerButton = self._buttons["_resizer_"..id]
			resizerButton:Show()
			resizerButton:SetPoint("LEFT", button, "RIGHT", (Theme.GetColSpacing() - RESIZER_WIDTH) / 2, 0)
			resizerButton:SetWidth(RESIZER_WIDTH)
			resizerButton:SetHitRectInsets(-RESIZER_WIDTH / 2, -RESIZER_WIDTH / 2, 0, 0)
		else
			local width = col:_GetWidth()
			if width then
				-- don't currently support auto-width with a header
				assert(width ~= true)
				button:SetWidth(width)
			else
				-- we found the button which will expand to fill the extra width, so reverse the iterator direction (only once)
				assert(isAscending)
				Table.InwardIteratorReverse(cols)
				if rightButton then
					-- anchor the right of this button to the left of the next one
					button:SetPoint("RIGHT", rightButton, "LEFT", -Theme.GetColSpacing(), 0)
				else
					-- this button is the last one, so anchor it to the right of our frame
					button:SetPoint("RIGHT", -Theme.GetColSpacing() / 2, 0)
				end
			end
		end
	end

	-- update the text, icons, and sort icons
	local sortBackground = self._frame.sortBackground
	local sortFlag = self._frame.sortFlag
	sortBackground:Hide()
	sortBackground:ClearAllPoints()
	sortFlag:Hide()
	sortFlag:ClearAllPoints()
	for _, col in ipairs(cols) do
		local id = col:_GetId()
		local button = self._buttons[id]
		if self._sortId == id then
			sortBackground:SetPoint("TOPLEFT", button, -Theme.GetColSpacing() / 2, 0)
			sortBackground:SetPoint("BOTTOMRIGHT", button, Theme.GetColSpacing() / 2, 0)
			sortBackground:SetColorTexture(Theme.GetColor("ACTIVE_BG"):GetFractionalRGBA())
			sortBackground:Show()
			sortFlag:SetPoint("TOPLEFT", button, -Theme.GetColSpacing() / 2, 2)
			sortFlag:SetPoint("TOPRIGHT", button, Theme.GetColSpacing() / 2, 2)
			sortFlag:SetColorTexture(Theme.GetColor(self._sortAscending and "INDICATOR" or "INDICATOR_ALT"):GetFractionalRGBA())
			sortFlag:Show()
		end
		local iconTexture = col:_GetTitleIcon()
		if iconTexture then
			local icon = self._icons[id]
			icon:Show()
			icon:ClearAllPoints()
			icon:SetPoint(col:_GetJustifyH(), button)
		else
			local text = self._texts[id]
			text:Show()
			text:ClearAllPoints()
			text:SetAllPoints(button)
		end
	end
end

function TableRow._LayoutDataRow(self)
	-- hide any hidden cols
	for _, col in self._tableInfo:_HiddenColIterator() do
		local id = col:_GetId()
		local icon = self._icons[id]

		local flag = self._icons["_flag_"..id]
		if flag then
			flag:Hide()
		end

		local expander = self._icons["_expander_"..id]
		if expander then
			expander:Hide()
			local expanderButton = self._buttons["_expander_"..id]
			expanderButton:Hide()
		end

		local check = self._icons["_check_"..id]
		if check then
			check:Hide()
		end

		if icon then
			icon:Hide()
		end

		if col:_HasText() then
			local text = self._texts[id]
			text:Hide()
		end

		local badge = self._texts["_badge_"..id]
		if badge then
			badge:Hide()
		end

		local numActionIcons = col:_GetActionIconInfo()
		for i = 1, numActionIcons do
			local actionIconKey = "_action"..i.."_"..id
			local actionIcon = self._rotatingIcons[actionIconKey]
			local actionIconButton = self._buttons[actionIconKey]
			actionIcon:Hide()
			actionIconButton:Hide()
		end

		if col:_HasTooltip() then
			local tooltipFrame = self._buttons[id]
			tooltipFrame:Hide()
		end
	end

	local cols = self._tableInfo:_GetVisibleCols()
	local canResize = self._scrollingTable:_CanResizeCols()
	local data = self:GetData()

	-- cache a list of the col ids
	wipe(private.colIdsTemp)
	for i, col in ipairs(cols) do
		private.colIdsTemp[i] = col:_GetId()
	end

	-- build from the left until we get to the col without a width, then switch to building from the right
	for i, col, isAscending in Table.InwardIterator(cols) do
		local id = private.colIdsTemp[i]
		local text = self._texts[id]
		local flag = self._icons["_flag_"..id]
		local expander = self._icons["_expander_"..id]
		local check = self._icons["_check_"..id]
		local badge = self._texts["_badge_"..id]
		local icon = self._icons[id]
		local width = canResize and self._scrollingTable:_GetColWidth(id) or col:_GetWidth()
		local hasText = col:_HasText()
		local textIndex = nil
		local numActionIcons, actionIconSize, actionIconShowOnHover = col:_GetActionIconInfo()
		local leftText = self:_GetDataLayoutRelativeFrame(i > 1 and private.colIdsTemp[i - 1], i > 1 and cols[i - 1], data, "LEFT")
		local rightText = self:_GetDataLayoutRelativeFrame(i < #cols and private.colIdsTemp[i + 1], i < #cols and cols[i + 1], data, "RIGHT")
		wipe(private.layoutTemp)
		local extraWidth = 0
		if not leftText and canResize then
			extraWidth = MORE_COL_WIDTH + Theme.GetColSpacing()
			private.LayoutTempInsertSpacing(MORE_COL_WIDTH + Theme.GetColSpacing())
		end

		if flag then
			local visible, color = false, nil
			if data then
				visible, color = col:_GetFlagState(data, self:IsMouseOver())
			end
			if visible then
				flag:Show()
				flag:SetHeight(self._frame:GetHeight() - FLAG_SPACING * 2)
				flag:SetColorTexture(color:GetFractionalRGBA())
				private.LayoutTempInsertSpacing(-FLAG_SPACING)
				private.LayoutTempInsertElementWithSpacing(flag, FLAG_SPACING)
			else
				flag:Hide()
			end
		end

		if expander then
			expander:Show()
			expander:ClearAllPoints()
			local visible, expanded, indentLevel, indentWidth, expanderSpacing, largeCaretIcons = false, false, 0, nil, nil, false
			if data then
				visible, expanded, indentLevel, indentWidth, expanderSpacing, largeCaretIcons = col:_GetExpanderState(data)
			end
			expanderSpacing = expanderSpacing or ICON_SPACING
			local expanderButton = self._buttons["_expander_"..id]
			if indentWidth or indentLevel < 0 then
				-- indent the expander itself
				private.LayoutTempInsertSpacing(indentWidth or (abs(indentLevel) * INDENT_WIDTH))
			end
			local texture = expanded and (largeCaretIcons and "iconPack.14x14/Caret/Down" or "iconPack.12x12/Caret/Down") or (largeCaretIcons and "iconPack.14x14/Caret/Right" or "iconPack.12x12/Caret/Right")
			local expanderWidth = TSM.UI.TexturePacks.GetWidth(texture)
			-- check if there is only spacing before the expander and the expander can fit within the spacing
			local firstLayoutElement = #private.layoutTemp == 1 and not leftText and private.layoutTemp[1] or nil
			if type(firstLayoutElement) == "number" and Math.Round(expanderWidth + expanderSpacing) <= Math.Round(firstLayoutElement) then
				private.layoutTemp[1] = private.layoutTemp[1] - expanderWidth - expanderSpacing
			end
			if visible then
				TSM.UI.TexturePacks.SetTextureAndSize(expander, texture)
				expander:Show()
				expanderButton:Show()
				private.LayoutTempInsertElementWithSpacing(expander, expanderSpacing)
			else
				expander:Hide()
				ScriptWrapper.Clear(expanderButton, "OnLeave")
				expanderButton:Hide()
				ScriptWrapper.Set(expanderButton, "OnLeave", private.ExpanderOnLeave, self)
				private.LayoutTempInsertSpacing(expanderWidth + expanderSpacing)
			end
			if not indentWidth and indentLevel > 0 then
				-- indent the other elements after the expander
				private.LayoutTempInsertSpacing(indentLevel * INDENT_WIDTH)
			end
		end

		if check then
			check:Show()
			check:ClearAllPoints()
			local visible = false
			if data then
				visible = col:_GetCheckState(data)
			end
			if visible then
				local checkWidth = check:GetWidth()
				-- check if there is only spacing before the check and the check can fit within the spacing
				local firstLayoutElement = #private.layoutTemp == 1 and not leftText and private.layoutTemp[1] or nil
				if type(firstLayoutElement) == "number" and Math.Round(checkWidth + ICON_SPACING) <= Math.Round(firstLayoutElement) then
					private.layoutTemp[1] = private.layoutTemp[1] - checkWidth - ICON_SPACING
				end
				check:Show()
				private.LayoutTempInsertElementWithSpacing(check, ICON_SPACING)
			else
				check:Hide()
			end
		end

		local iconTexture = icon and icon:GetTexture()
		if iconTexture then
			icon:Show()
			icon:ClearAllPoints()
			textIndex = #private.layoutTemp + 1
			private.LayoutTempInsertElementWithSpacing(icon, ICON_SPACING)
		end

		if hasText then
			text:Show()
			text:ClearAllPoints()
			textIndex = textIndex or #private.layoutTemp + 1
			private.LayoutTempInsertElementWithSpacing(text, ICON_SPACING)
		end

		if badge then
			badge:ClearAllPoints()
			private.LayoutTempInsertElementWithSpacing(badge, ICON_SPACING)
			local visible, value = false, nil
			if data then
				visible, value = col:_GetBadgeState(data)
			end
			if visible then
				badge:SetText(value)
				badge:Show()
			else
				badge:Hide()
			end
		end

		local hadVisibleRightActionIcon = false
		for j = 1, numActionIcons do
			local actionIconKey = "_action"..j.."_"..id
			local actionIcon = self._rotatingIcons[actionIconKey]
			local actionIconButton = self._buttons[actionIconKey]
			actionIcon:ClearAllPoints()
			actionIconButton:Show()
			local visible, texture, isLeft = false, nil, false
			if data and (not actionIconShowOnHover or self._state) then
				local _, shouldRotate = nil, nil
				visible, texture, isLeft, _, shouldRotate = col:_GetActionIcon(data, j, actionIconButton:IsMouseOver())
				if shouldRotate then
					actionIcon.ag:Play()
				else
					actionIcon.ag:Stop()
				end
			end
			if visible then
				hadVisibleRightActionIcon = hadVisibleRightActionIcon or not isLeft
				if isLeft and textIndex then
					-- check if there is only padding before the text, and if this icon can fit within that padding
					local firstLayoutElement = textIndex == 2 and not leftText and private.layoutTemp[1] or nil
					if textIndex == 2 and type(firstLayoutElement) == "number" and Math.Round(actionIconSize + ICON_SPACING) <= Math.Round(firstLayoutElement) then
						private.layoutTemp[1] = private.layoutTemp[1] - actionIconSize - ICON_SPACING
					end
					private.LayoutTempInsertElementWithSpacing(actionIcon, ICON_SPACING, textIndex)
					textIndex = textIndex + 2
				else
					private.LayoutTempInsertElementWithSpacing(actionIcon, ICON_SPACING)
				end
				actionIcon:Show()
				actionIconButton:SetMouseClickEnabled(true)
				if type(texture) == "string" and TSM.UI.TexturePacks.IsValid(texture) then
					TSM.UI.TexturePacks.SetTexture(actionIcon, texture)
				else
					actionIcon:SetTexture(texture)
				end
			else
				actionIcon:Hide()
				actionIconButton:SetMouseClickEnabled(false)
			end
		end

		local usedWidth = 0
		local lastElement = text
		if hasText then
			local didText = false
			local prevElement = nil
			local currentSpacing = 0
			local isInverted = false
			local primaryText = (isAscending and leftText) or (not isAscending and rightText) or nil
			local primarySide = isAscending and "LEFT" or "RIGHT"
			local secondarySide = isAscending and "RIGHT" or "LEFT"
			local spacingModifier = isAscending and 1 or -1
			for j = (isAscending and 1 or #private.layoutTemp), (isAscending and #private.layoutTemp or 1), (isAscending and 1 or -1) do
				local element = private.layoutTemp[j]
				if type(element) == "number" then
					currentSpacing = currentSpacing + element
				else
					if prevElement then
						isInverted = isInverted or (prevElement == text and not width and numActionIcons > 0)
						if isInverted then
							assert(isAscending)
							-- this is an icon to the right of the text and the text will need to expand, so invert the anchoring
							prevElement:SetPoint("RIGHT", element, "LEFT", -currentSpacing, 0)
						else
							element:SetPoint(primarySide, prevElement, secondarySide, currentSpacing * spacingModifier, 0)
						end
					elseif primaryText then
						element:SetPoint(primarySide, primaryText, secondarySide, (currentSpacing + Theme.GetColSpacing()) * spacingModifier, 0)
					else
						if element == text and not canResize then
							currentSpacing = currentSpacing + Theme.GetColSpacing() / 2
						end
						element:SetPoint(primarySide, (currentSpacing + Theme.GetColSpacing() / 2) * spacingModifier, 0)
					end
					-- don't use up width for elements to the right of the text which aren't visible
					if isAscending and (not didText or element:IsVisible()) then
						-- the text will resize to fill any remaining width, so don't count it in the used width
						if element ~= text then
							usedWidth = usedWidth + element:GetWidth()
						end
						usedWidth = usedWidth + currentSpacing
					end
					prevElement = element
					currentSpacing = 0
					didText = didText or element == text
				end
			end
			lastElement = prevElement
		else
			-- center the icon in the middle
			-- TODO: support other combinations
			assert(isAscending and icon and not badge and not expander)
			if leftText then
				text:SetPoint("LEFT", leftText, "RIGHT", Theme.GetColSpacing(), 0)
			else
				text:SetPoint("LEFT", Theme.GetColSpacing() / 2, 0)
			end
			icon:SetPoint("CENTER", text, extraWidth / 2, 0)
		end

		if col:_HasTooltip() then
			local tooltipFrame = self._buttons[id]
			tooltipFrame:Show()
			tooltipFrame:SetPoint("LEFT", iconTexture and icon or text)
			tooltipFrame:SetPoint("RIGHT", text)
		end

		if width then
			if hasText then
				if width == true then
					-- auto-width
					text:SetWidth(10000)
					width = text:GetStringWidth()
				elseif not rightText then
					-- leave an extra COL_SPACING / 2 of margin on the right of the last col
					extraWidth = extraWidth - Theme.GetColSpacing() / 2
				end
				text:SetWidth(width - usedWidth + extraWidth)
			else
				assert(width ~= true) -- auto-width requires the cell to have text
				text:SetWidth(width + extraWidth)
			end
		else
			-- we found the text which will expand to fill the extra width, so reverse the iterator direction (only once)
			assert(isAscending and not canResize)
			Table.InwardIteratorReverse(cols)
			if rightText then
				-- anchor the right of this the last element for this col to the left of the next one
				lastElement:SetPoint("RIGHT", rightText, "LEFT", -Theme.GetColSpacing(), 0)
			else
				-- this col is the last one, so anchor it to the right of our frame
				local spacing = Theme.GetColSpacing()
				if hadVisibleRightActionIcon and not rightText then
					-- a bit of extra spacing to the right of the last action icon
					spacing = spacing + ICON_SPACING
				end
				lastElement:SetPoint("RIGHT", -spacing, 0)
			end
		end
	end
end

function TableRow._GetDataLayoutRelativeFrame(self, id, col, data, side)
	if not id then
		return nil
	end
	assert(side == "LEFT" or side == "RIGHT")
	local numActionIcons, _, actionIconShowOnHover = col:_GetActionIconInfo()
	if data and (not actionIconShowOnHover or self._state) then
		for i = (side == "RIGHT" and 1 or numActionIcons), (side == "RIGHT" and numActionIcons or 1), (side == "RIGHT" and 1 or -1) do
			local actionIconKey = "_action"..i.."_"..id
			local actionIcon = self._rotatingIcons[actionIconKey]
			local actionIconButton = self._buttons[actionIconKey]
			local visible, _, isLeft = col:_GetActionIcon(data, i, actionIconButton:IsMouseOver())
			if visible and ((isLeft and side == "RIGHT") or (not isLeft and side == "LEFT")) then
				return actionIcon
			end
		end
	end
	local badge = self._texts["_badge_"..id]
	if side == "LEFT" and badge and badge:IsVisible() then
		return badge
	else
		return self._texts[id]
	end
end

function TableRow._LookupIconByButton(self, button)
	local key = Table.GetDistinctKey(self._buttons, button)
	local id = strmatch(key, "^_icon_(.+)$")
	local col = self._tableInfo:GetColById(id)
	return col, self._icons[id]
end

function TableRow._LookupActionIconByButton(self, button)
	local key = Table.GetDistinctKey(self._buttons, button)
	local iconIndex, id = strmatch(key, "^_action(%d+)_(.+)$")
	iconIndex = tonumber(iconIndex)
	assert(iconIndex and id)
	local col = self._tableInfo:GetColById(id)
	return col, iconIndex, self._rotatingIcons[key]
end

function TableRow._LookupTooltipByFrame(self, frame)
	local col = self._tableInfo:GetColById(Table.GetDistinctKey(self._buttons, frame))
	if not col:_HasTooltip() then
		return nil
	end
	local tooltip, noWrap, anchorFrame = col:_GetTooltip(self:GetData())
	local linkingDisabled = col:_GetTooltipLinkingDisabled()
	return tooltip, noWrap, anchorFrame, linkingDisabled
end



-- ============================================================================
-- TableRow - Local Script Handlers
-- ============================================================================

function private.MenuDialogIterator(self, prevIndex)
	if prevIndex == nil then
		return "HIDE", L["Hide Columns"], private.MenuDialogHideColIterator
	elseif prevIndex == "HIDE" then
		return "LOCK", self._scrollingTable:_IsColWidthLocked() and L["Unlock Column Width"] or L["Lock Column Width"]
	elseif prevIndex == "LOCK" then
		return "RESET", L["Reset Table"]
	else
		return self._tableInfo:_MenuDialogIterator(prevIndex)
	end
end

function private.GetMoreDialogColRowContent(col)
	local color = Theme.GetColor(col:_IsHidden() and "TEXT+DISABLED" or "TEXT")
	local titleText = col:_GetTitle()
	if titleText then
		return color:ColorText(titleText)
	end
	local titleIcon = col:_GetTitleIcon()
	if titleIcon then
		local textureKey = TSM.UI.TexturePacks.GetColoredKey(titleIcon, color)
		return TSM.UI.TexturePacks.GetTextureLink(textureKey)
	end
	error("Unknown title")
end

function private.MenuDialogHideColIterator(self, prevIndex)
	local foundPrevIndex = prevIndex == nil
	for _, col in ipairs(self._tableInfo:_GetCols()) do
		if foundPrevIndex and col:_CanHide() then
			return col:_GetId(), private.GetMoreDialogColRowContent(col)
		elseif col:_GetId() == prevIndex then
			foundPrevIndex = true
		end
	end
end

function private.MoreMenuDialogButtonOnClick(button, self, index1, index2, extra)
	assert(not extra and index1)
	if index1 == "HIDE" then
		assert(index2)
		local col = self._tableInfo:GetColById(index2)
		assert(col:_CanHide())
		self._scrollingTable:_ToggleColHide(index2)
		-- update the button text
		button:SetText(private.GetMoreDialogColRowContent(col))
			:Draw()
	elseif index1 == "LOCK" then
		assert(not index2)
		self._scrollingTable:GetBaseElement():HideDialog()
		self._scrollingTable:_ToogleColWidthLocked()
	elseif index1 == "RESET" then
		assert(not index2)
		self._scrollingTable:GetBaseElement():HideDialog()
		self._scrollingTable:_ResetContext()
	else
		self._tableInfo:_HandleMenuButtonClick(index1, index2)
	end
end

function private.MoreColOnClick(self, mouseButton)
	self._scrollingTable:GetBaseElement():ShowMenuDialog(self._frame, private.MenuDialogIterator, self, private.MoreMenuDialogButtonOnClick)
end

function private.HeaderColOnClick(button, mouseButton)
	local self = private.rowFrameLookup[button:GetParent()]
	if mouseButton == "LeftButton" then
		self._scrollingTable:_ToggleSort(Table.GetDistinctKey(self._buttons, button))
	end
end

function private.HeaderColOnEnter(button)
	local self = private.rowFrameLookup[button:GetParent()]
	local col = self._tableInfo:GetColById(Table.GetDistinctKey(self._buttons, button))
	local tooltip = col:_GetHeaderTooltip()
	if tooltip then
		Tooltip.Show(button, tooltip)
	end
end

function private.HeaderColOnLeave(button)
	local self = private.rowFrameLookup[button:GetParent()]
	local col = self._tableInfo:GetColById(Table.GetDistinctKey(self._buttons, button))
	if col:_GetHeaderTooltip() then
		Tooltip.Hide()
	end
end

function private.RowOnMouseDown(self, button)
	if button ~= "RightButton" then
		return
	end
	private.RowOnClick(self, "LeftButton")
end

function private.RowOnClick(self, mouseButton)
	if mouseButton == "LeftButton" and not self._scrollingTable._selectionDisabled then
		self._scrollingTable:SetSelection(self:GetData())
	end
	if self._scrollingTable:_IsSelected(self:GetData()) then
		self:SetHighlightState(self._scrollingTable._selectionDisabled and "selected" or "selectedHover")
	else
		self:SetHighlightState("hover")
	end
	self._scrollingTable:_HandleRowClick(self:GetData(), mouseButton)
end

function private.RowOnEnter(self)
	if self._scrollingTable:_IsSelected(self:GetData()) then
		self:SetHighlightState(self._scrollingTable._selectionDisabled and "selected" or "selectedHover")
	else
		self:SetHighlightState("hover")
	end
	if self._scrollingTable._rightClickToggle and IsMouseButtonDown("RightButton") then
		private.RowOnClick(self, "LeftButton")
	end
	local cursor = self._tableInfo:_GetCursor()
	if cursor then
		SetCursor(cursor)
	end
	ScriptWrapper.Set(self._frame, "OnUpdate", private.RowOnUpdate, self)
end

function private.RowOnLeave(self)
	for _, text in pairs(self._texts) do
		if text._hoverStart then
			Tooltip.Hide()
			text._hoverStart = nil
		end
	end
	ScriptWrapper.Clear(self._frame, "OnUpdate")
	if self:IsVisible() then
		if self._scrollingTable:_IsSelected(self:GetData()) then
			self:SetHighlightState("selected")
		else
			self:SetHighlightState(nil)
		end
	end
	if self._tableInfo:_GetCursor() then
		ResetCursor()
	end
end

function private.RowOnUpdate(self)
	if not self._frame:IsMouseOver() then
		private.RowOnLeave(self)
		return
	end
	local tooltipText = nil
	for _, tooltipFrame in pairs(self._buttons) do
		if tooltipFrame:IsVisible() and tooltipFrame:IsMouseOver() then
			return
		end
	end
	for _, text in pairs(self._texts) do
		if text:IsMouseOver() and text:GetWidth() + 0.5 < text:GetUnboundedStringWidth() then
			text._hoverStart = text._hoverStart or GetTime()
			tooltipText = text
		else
			text._hoverStart = nil
		end
	end
	if not tooltipText or tooltipText:GetText() == "" or GetTime() - tooltipText._hoverStart < FULL_TEXT_TOOLTIP_DELAY_S then
		Tooltip.Hide()
		return
	end
	Tooltip.Show(tooltipText, tooltipText:GetText(), true)
end

function private.IconButtonOnEnter(button)
	local self = private.rowFrameLookup[button:GetParent()]
	self._frame:GetScript("OnEnter")(self._frame)
	local col, icon = self:_LookupIconByButton(button)
	local texture, tooltip = col:_GetIcon(self:GetData(), true)
	if type(texture) == "string" and TSM.UI.TexturePacks.IsValid(texture) then
		TSM.UI.TexturePacks.SetTexture(icon, texture)
	else
		icon:SetTexture(texture)
	end
	if tooltip then
		Tooltip.Show(button, tooltip)
	end
end

function private.IconButtonOnLeave(button)
	local self = private.rowFrameLookup[button:GetParent()]
	self._frame:GetScript("OnLeave")(self._frame)
	local col, icon = self:_LookupIconByButton(button)
	local texture, tooltip = col:_GetIcon(self:GetData(), false)
	if type(texture) == "string" and TSM.UI.TexturePacks.IsValid(texture) then
		TSM.UI.TexturePacks.SetTexture(icon, texture)
	else
		icon:SetTexture(texture)
	end
	if tooltip then
		Tooltip.Hide()
	end
end

function private.IconButtonOnClick(button, mouseButton)
	local self = private.rowFrameLookup[button:GetParent()]
	local col = self:_LookupIconByButton(button)
	col:_OnIconClick(self:GetData(), mouseButton)
end

function private.ActionIconButtonOnEnter(button)
	local self = private.rowFrameLookup[button:GetParent()]
	self._frame:GetScript("OnEnter")(self._frame)
	local col, iconIndex, icon = self:_LookupActionIconByButton(button)
	local visible, texture, _, tooltip = col:_GetActionIcon(self:GetData(), iconIndex, true)
	if not visible then
		return
	end
	if tooltip then
		Tooltip.Show(button, tooltip)
	end
	if type(texture) == "string" and TSM.UI.TexturePacks.IsValid(texture) then
		TSM.UI.TexturePacks.SetTexture(icon, texture)
	else
		icon:SetTexture(texture)
	end
end

function private.ActionIconButtonOnLeave(button)
	if not button:IsVisible() then
		-- ignore OnLeave if we just hid this button
		Tooltip.Hide()
		return
	end
	local self = private.rowFrameLookup[button:GetParent()]
	self._frame:GetScript("OnLeave")(self._frame)
	local col, iconIndex, icon, tooltip = self:_LookupActionIconByButton(button)
	local visible, texture = col:_GetActionIcon(self:GetData(), iconIndex, false)
	if tooltip then
		Tooltip.Hide()
	end
	if not visible then
		return
	end
	if type(texture) == "string" and TSM.UI.TexturePacks.IsValid(texture) then
		TSM.UI.TexturePacks.SetTexture(icon, texture)
	else
		icon:SetTexture(texture)
	end
end

function private.ActionIconButtonOnClick(button, mouseButton)
	local self = private.rowFrameLookup[button:GetParent()]
	local col, iconIndex = self:_LookupActionIconByButton(button)
	col:_OnActionButtonClick(self:GetData(), iconIndex, mouseButton)
end

function private.TooltipFrameOnEnter(frame)
	frame.isShowingTooltip = true
	local self = private.rowFrameLookup[frame:GetParent()]
	self._frame:GetScript("OnEnter")(self._frame)
	local tooltip, noWrap, anchorFrame = self:_LookupTooltipByFrame(frame)
	Tooltip.Show(anchorFrame and self._frame or frame, tooltip, noWrap)
end

function private.TooltipFrameOnLeave(frame)
	frame.isShowingTooltip = nil
	local self = private.rowFrameLookup[frame:GetParent()]
	self._frame:GetScript("OnLeave")(self._frame)
	Tooltip.Hide()
end

function private.TooltipFrameOnClick(frame, ...)
	local self = private.rowFrameLookup[frame:GetParent()]
	local tooltip, _, _, linkingDisabled = self:_LookupTooltipByFrame(frame)
	if not linkingDisabled and (IsShiftKeyDown() or IsControlKeyDown()) then
		local link = tooltip and ItemInfo.GetLink(tooltip)
		if link then
			if IsShiftKeyDown() then
				Wow.SafeItemRef(link)
			elseif IsControlKeyDown() then
				DressUpItemLink(link)
			end
			return
		end
	end
	self._frame:GetScript("OnClick")(self._frame, ...)
end

function private.ResizerOnEnter(button)
	local self = private.rowFrameLookup[button:GetParent()]
	local id = Table.GetDistinctKey(self._buttons, button)
	self._icons[id]:Show()
end

function private.ResizerOnLeave(button)
	local self = private.rowFrameLookup[button:GetParent()]
	local id = Table.GetDistinctKey(self._buttons, button)
	self._icons[id]:Hide()
end

function private.ResizerOnMouseDown(button, mouseButton)
	if mouseButton ~= "LeftButton" then
		return
	end
	ScriptWrapper.Set(button, "OnUpdate", private.ResizerOnUpdate)
	local self = private.rowFrameLookup[button:GetParent()]
	local id = Table.GetDistinctKey(self._buttons, button)
	local parentId = strmatch(id, "^_resizer_(.+)$")
	self._buttons[parentId]:StartSizing("RIGHT")
	self._icons[id]:SetPoint("TOPLEFT", self._buttons[parentId], "TOPLEFT", -Theme.GetColSpacing() / 2, 0)
end

function private.ResizerOnMouseUp(button, mouseButton)
	if mouseButton ~= "LeftButton" then
		return
	end
	ScriptWrapper.Clear(button, "OnUpdate")
	local self = private.rowFrameLookup[button:GetParent()]
	local id = Table.GetDistinctKey(self._buttons, button)
	local parentId = strmatch(id, "^_resizer_(.+)$")
	local parentButton = self._buttons[parentId]
	parentButton:StopMovingOrSizing()
	self:_LayoutHeaderRow()
	self._scrollingTable:_SetColWidth(parentId, Math.Round(parentButton:GetWidth()), true)
	self._icons[id]:SetPoint("TOPLEFT", button)
end

function private.ResizerOnClick(button, mouseButton)
	if mouseButton ~= "RightButton" then
		return
	end
	local self = private.rowFrameLookup[button:GetParent()]
	local id = Table.GetDistinctKey(self._buttons, button)
	local parentId = strmatch(id, "^_resizer_(.+)$")
	self._scrollingTable:_ResetColWidth(parentId)
end

function private.ResizerOnUpdate(button)
	local self = private.rowFrameLookup[button:GetParent()]
	local id = Table.GetDistinctKey(self._buttons, button)
	local parentId = strmatch(id, "^_resizer_(.+)$")
	local parentButton = self._buttons[parentId]
	self._scrollingTable:_SetColWidth(parentId, Math.Round(parentButton:GetWidth()))
end

function private.ExpanderOnLeave(self)
	self._frame:GetScript("OnEnter")(self._frame)
end

function private.ExpanderOnClick(self, mouseButton, ...)
	if mouseButton ~= "LeftButton" then
		return
	end
	self._frame:GetScript("OnDoubleClick")(self._frame, mouseButton, ...)
end



-- ============================================================================
-- Private Helper Functions
-- ============================================================================

function private.LayoutTempInsertSpacing(spacing)
	assert(type(spacing) == "number")
	local length = #private.layoutTemp
	if type(private.layoutTemp[length]) == "number" then
		-- add to the previous spacing
		private.layoutTemp[length] = private.layoutTemp[length] + spacing
	else
		-- insert a new entry
		private.layoutTemp[length + 1] = spacing
	end
end

function private.LayoutTempInsertElementWithSpacing(element, spacing, index)
	assert(type(element) == "table" and type(spacing) == "number")
	if index then
		tinsert(private.layoutTemp, index, element)
		tinsert(private.layoutTemp, index + 1, spacing)
	else
		local length = #private.layoutTemp
		private.layoutTemp[length + 1] = element
		private.layoutTemp[length + 2] = spacing
	end
end
