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

--- ApplicationFrame UI Element Class.
-- An application frame is the base frame of all of the TSM UIs. It is a subclass of the @{Frame} class.
-- @classmod ApplicationFrame

local _, TSM = ...
local L = TSM.Include("Locale").GetTable()
local Math = TSM.Include("Util.Math")
local TempTable = TSM.Include("Util.TempTable")
local Color = TSM.Include("Util.Color")
local NineSlice = TSM.Include("Util.NineSlice")
local Table = TSM.Include("Util.Table")
local ScriptWrapper = TSM.Include("Util.ScriptWrapper")
local Theme = TSM.Include("Util.Theme")
local Tooltip = TSM.Include("UI.Tooltip")
local UIElements = TSM.Include("UI.UIElements")
local ApplicationFrame = TSM.Include("LibTSMClass").DefineClass("ApplicationFrame", TSM.UI.Frame)
UIElements.Register(ApplicationFrame)
TSM.UI.ApplicationFrame = ApplicationFrame
local private = {
	menuDialogContext = {},
}
local SECONDS_PER_HOUR = 60 * 60
local SECONDS_PER_DAY = 24 * SECONDS_PER_HOUR
local CONTENT_FRAME_OFFSET = 8
local DIALOG_RELATIVE_LEVEL = 18
local HEADER_HEIGHT = 40
local MIN_SCALE = 0.3
local DIALOG_OPACITY_PCT = 65
local MIN_ON_SCREEN_PX = 50
local function NoOp() end



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

function ApplicationFrame.__init(self)
	self.__super:__init()
	self._contentFrame = nil
	self._contextTable = nil
	self._defaultContextTable = nil
	self._isScaling = nil
	self._protected = nil
	self._minWidth = 0
	self._minHeight = 0
	self._dialogStack = {}

	local frame = self:_GetBaseFrame()
	local globalFrameName = tostring(frame)
	_G[globalFrameName] = frame
	-- insert our frames before other addons (i.e. Skillet) to avoid conflicts
	tinsert(UISpecialFrames, 1, globalFrameName)

	self._nineSlice = NineSlice.New(frame)
	self._nineSlice:SetStyle("outerFrame")

	frame.resizeIcon = frame:CreateTexture(nil, "ARTWORK")
	frame.resizeIcon:SetPoint("BOTTOMRIGHT")
	TSM.UI.TexturePacks.SetTextureAndSize(frame.resizeIcon, "iconPack.14x14/Resize")

	frame.resizeBtn = CreateFrame("Button", nil, frame)
	frame.resizeBtn:SetAllPoints(frame.resizeIcon)
	frame.resizeBtn:RegisterForClicks("LeftButtonUp", "RightButtonUp")
	ScriptWrapper.Set(frame.resizeBtn, "OnEnter", private.ResizeButtonOnEnter, self)
	ScriptWrapper.Set(frame.resizeBtn, "OnLeave", private.ResizeButtonOnLeave, self)
	ScriptWrapper.Set(frame.resizeBtn, "OnMouseDown", private.ResizeButtonOnMouseDown, self)
	ScriptWrapper.Set(frame.resizeBtn, "OnMouseUp", private.ResizeButtonOnMouseUp, self)
	ScriptWrapper.Set(frame.resizeBtn, "OnClick", private.ResizeButtonOnClick, self)
	Theme.RegisterChangeCallback(function()
		if self:IsVisible() then
			self:Draw()
		end
	end)
end

function ApplicationFrame.Acquire(self)
	self:AddChildNoLayout(UIElements.New("Frame", "titleFrame")
		:SetLayout("HORIZONTAL")
		:SetHeight(24)
		:AddAnchor("TOPLEFT", 8, -8)
		:AddAnchor("TOPRIGHT", -8, -8)
		:SetBackgroundColor("FRAME_BG")
		:AddChild(UIElements.New("Texture", "icon")
			:SetMargin(0, 16, 0, 0)
			:SetTextureAndSize("uiFrames.SmallLogo")
		)
		:AddChild(UIElements.New("Text", "title")
			:AddAnchor("CENTER")
			:SetWidth("AUTO")
			:SetFont("BODY_BODY2_BOLD")
			:SetTextColor("TEXT_ALT")
		)
		:AddChild(UIElements.New("Spacer", "spacer"))
		:AddChild(UIElements.New("Button", "closeBtn")
			:SetBackgroundAndSize("iconPack.24x24/Close/Default")
			:SetScript("OnClick", private.CloseButtonOnClick)
		)
	)
	self.__super:Acquire()
	local frame = self:_GetBaseFrame()
	frame:EnableMouse(true)
	frame:SetMovable(true)
	frame:SetResizable(true)
	frame:RegisterForDrag("LeftButton")
	self:SetScript("OnDragStart", private.FrameOnDragStart)
	self:SetScript("OnDragStop", private.FrameOnDragStop)
end

function ApplicationFrame.Release(self)
	if self._protected then
		tinsert(UISpecialFrames, 1, tostring(self:_GetBaseFrame()))
	end
	self._contentFrame = nil
	self._contextTable = nil
	self._defaultContextTable = nil
	self:_GetBaseFrame():SetMinResize(0, 0)
	self:_GetBaseFrame():SetMaxResize(0, 0)
	self._isScaling = nil
	self._protected = nil
	self._minWidth = 0
	self._minHeight = 0
	self.__super:Release()
end

--- Adds player gold text to the title frame.
-- @tparam ApplicationFrame self The application frame object
-- @treturn ApplicationFrame The application frame object
function ApplicationFrame.AddPlayerGold(self)
	local titleFrame = self:GetElement("titleFrame")
	titleFrame:AddChildBeforeById(titleFrame:HasChildById("switchBtn") and "switchBtn" or "closeBtn", UIElements.New("PlayerGoldText", "playerGold")
		:SetWidth("AUTO")
		:SetMargin(0, 8, 0, 0)
	)
	return self
end

--- Adds the app status icon to the title frame.
-- @tparam ApplicationFrame self The application frame object
-- @treturn ApplicationFrame The application frame object
function ApplicationFrame.AddAppStatusIcon(self)
	local color, texture = nil, nil
	local appUpdateAge = time() - TSM.GetAppUpdateTime()
	local auctionDBRealmTime, auctionDBRegionTime = TSM.AuctionDB.GetAppDataUpdateTimes()
	local auctionDBRealmAge = time() - auctionDBRealmTime
	local auctionDBRegionAge = time() - auctionDBRegionTime
	if appUpdateAge >= 2 * SECONDS_PER_DAY or auctionDBRealmAge > 2 * SECONDS_PER_DAY or auctionDBRegionAge > 2 * SECONDS_PER_DAY then
		color = "RED"
		texture = "iconPack.14x14/Attention"
	elseif appUpdateAge >= 2 * SECONDS_PER_HOUR or auctionDBRealmAge >= 4 * SECONDS_PER_HOUR then
		color = "YELLOW"
		texture = "iconPack.14x14/Attention"
	else
		color = "GREEN"
		texture = "iconPack.14x14/Checkmark/Circle"
	end
	local titleFrame = self:GetElement("titleFrame")
	titleFrame:AddChildBeforeById("playerGold", UIElements.New("Button", "playerGold")
		:SetBackgroundAndSize(TSM.UI.TexturePacks.GetColoredKey(texture, Theme.GetFeedbackColor(color)))
		:SetMargin(0, 8, 0, 0)
		:SetTooltip(private.GetAppStatusTooltip)
	)
	return self
end

--- Adds a switch button to the title frame.
-- @tparam ApplicationFrame self The application frame object
-- @tparam function onClickHandler The handler for the OnClick script for the button
-- @treturn ApplicationFrame The application frame object
function ApplicationFrame.AddSwitchButton(self, onClickHandler)
	local titleFrame = self:GetElement("titleFrame")
	titleFrame:AddChildBeforeById("closeBtn", UIElements.New("ActionButton", "switchBtn")
		:SetSize(95, 20)
		:SetMargin(0, 8, 0, 0)
		:SetFont("BODY_BODY3_MEDIUM")
		:SetText(L["WOW UI"])
		:SetScript("OnClick", onClickHandler)
	)
	return self
end

function ApplicationFrame.SetProtected(self, protected)
	self._protected = protected
	local globalFrameName = tostring(self:_GetBaseFrame())
	if protected then
		Table.RemoveByValue(UISpecialFrames, globalFrameName)
	else
		if not Table.KeyByValue(UISpecialFrames, globalFrameName) then
			-- insert our frames before other addons (i.e. Skillet) to avoid conflicts
			tinsert(UISpecialFrames, 1, globalFrameName)
		end
	end
	return self
end

--- Sets the title text.
-- @tparam ApplicationFrame self The application frame object
-- @tparam string title The title text
-- @treturn ApplicationFrame The application frame object
function ApplicationFrame.SetTitle(self, title)
	local titleFrame = self:GetElement("titleFrame")
	titleFrame:GetElement("title"):SetText(title)
	titleFrame:Draw()
	return self
end

--- Sets the content frame.
-- @tparam ApplicationFrame self The application frame object
-- @tparam Frame frame The frame's content frame
-- @treturn ApplicationFrame The application frame object
function ApplicationFrame.SetContentFrame(self, frame)
	assert(frame:__isa(TSM.UI.Frame))
	frame:WipeAnchors()
	frame:AddAnchor("TOPLEFT", CONTENT_FRAME_OFFSET, -HEADER_HEIGHT)
	frame:AddAnchor("BOTTOMRIGHT", -CONTENT_FRAME_OFFSET, CONTENT_FRAME_OFFSET)
	frame:SetPadding(2)
	frame:SetBorderColor("ACTIVE_BG", 2)
	self._contentFrame = frame
	self:AddChildNoLayout(frame)
	return self
end

--- Sets the context table.
-- This table can be used to preserve position and size information across lifecycles of the application frame and even
-- WoW sessions if it's within the settings DB.
-- @tparam ApplicationFrame self The application frame object
-- @tparam table tbl The context table
-- @tparam table defaultTbl Default values (required attributes: `width`, `height`, `centerX`, `centerY`)
-- @treturn ApplicationFrame The application frame object
function ApplicationFrame.SetContextTable(self, tbl, defaultTbl)
	assert(defaultTbl.width > 0 and defaultTbl.height > 0)
	assert(defaultTbl.centerX and defaultTbl.centerY)
	tbl.width = tbl.width or defaultTbl.width
	tbl.height = tbl.height or defaultTbl.height
	tbl.centerX = tbl.centerX or defaultTbl.centerX
	tbl.centerY = tbl.centerY or defaultTbl.centerY
	tbl.scale = tbl.scale or defaultTbl.scale
	self._contextTable = tbl
	self._defaultContextTable = defaultTbl
	return self
end

--- Sets the context table from a settings object.
-- @tparam ApplicationFrame self The application frame object
-- @tparam Settings settings The settings object
-- @tparam string key The setting key
-- @treturn ApplicationFrame The application frame object
function ApplicationFrame.SetSettingsContext(self, settings, key)
	return self:SetContextTable(settings[key], settings:GetDefaultReadOnly(key))
end

--- Sets the minimum size the application frame can be resized to.
-- @tparam ApplicationFrame self The application frame object
-- @tparam number minWidth The minimum width
-- @tparam number minHeight The minimum height
-- @treturn ApplicationFrame The application frame object
function ApplicationFrame.SetMinResize(self, minWidth, minHeight)
	self._minWidth = minWidth
	self._minHeight = minHeight
	return self
end

--- Shows a dialog frame.
-- @tparam ApplicationFrame self The application frame object
-- @tparam Element frame The element to show in a dialog
-- @param context The context to set on the dialog frame
function ApplicationFrame.ShowDialogFrame(self, frame, context)
	local dialogFrame = UIElements.New("Frame", "_dialog_"..random())
		:SetRelativeLevel(DIALOG_RELATIVE_LEVEL * (#self._dialogStack + 1))
		:SetBackgroundColor(Color.GetFullBlack():GetOpacity(DIALOG_OPACITY_PCT))
		:AddAnchor("TOPLEFT")
		:AddAnchor("BOTTOMRIGHT")
		:SetMouseEnabled(true)
		:SetMouseWheelEnabled(true)
		:SetContext(context)
		:SetScript("OnMouseWheel", NoOp)
		:SetScript("OnMouseUp", private.DialogOnMouseUp)
		:SetScript("OnHide", private.DialogOnHide)
		:AddChildNoLayout(frame)
	tinsert(self._dialogStack, dialogFrame)
	self._contentFrame:AddChildNoLayout(dialogFrame)
	dialogFrame:Show()
	dialogFrame:Draw()
end

--- Show a confirmation dialog.
-- @tparam ApplicationFrame self The application frame object
-- @tparam string title The title of the dialog
-- @tparam string subTitle The sub-title of the dialog
-- @tparam function callback The callback for when the dialog is closed
-- @tparam[opt] varag ... Arguments to pass to the callback
function ApplicationFrame.ShowConfirmationDialog(self, title, subTitle, callback, ...)
	local context = TempTable.Acquire(...)
	context.callback = callback
	local frame = UIElements.New("Frame", "frame")
		:SetLayout("VERTICAL")
		:SetSize(328, 158)
		:SetPadding(12, 12, 8, 12)
		:AddAnchor("CENTER")
		:SetBackgroundColor("FRAME_BG", true)
		:SetMouseEnabled(true)
		:AddChild(UIElements.New("Frame", "header")
			:SetLayout("HORIZONTAL")
			:SetHeight(24)
			:AddChild(UIElements.New("Text", "title")
				:SetHeight(20)
				:SetMargin(32, 8, 0, 0)
				:SetFont("BODY_BODY2_BOLD")
				:SetJustifyH("CENTER")
				:SetText(title)
			)
			:AddChild(UIElements.New("Button", "closeBtn")
				:SetBackgroundAndSize("iconPack.24x24/Close/Default")
				:SetScript("OnClick", private.DialogCancelBtnOnClick)
			)
		)
		:AddChild(UIElements.New("Text", "desc")
			:SetMargin(0, 0, 16, 16)
			:SetFont("BODY_BODY3")
			:SetJustifyH("LEFT")
			:SetJustifyV("TOP")
			:SetText(subTitle)
		)
		:AddChild(UIElements.New("ActionButton", "confirmBtn")
			:SetHeight(24)
			:SetText(L["Confirm"])
			:SetScript("OnClick", private.DialogConfirmBtnOnClick)
		)
	self:ShowDialogFrame(frame, context)
end

--- Show a dialog triggered by a "more" button.
-- @tparam ApplicationFrame self The application frame object
-- @tparam Button moreBtn The "more" button
-- @tparam function iter A dialog menu row iterator with the following fields: `index, text, callback`
function ApplicationFrame.ShowMoreButtonDialog(self, moreBtn, iter)
	local frame = UIElements.New("PopupFrame", "moreDialog")
		:SetLayout("VERTICAL")
		:SetWidth(200)
		:SetPadding(0, 0, 8, 4)
		:AddAnchor("TOPRIGHT", moreBtn:_GetBaseFrame(), "BOTTOM", 22, -16)
	local numRows = 0
	for i, text, callback in iter do
		frame:AddChild(UIElements.New("Button", "row"..i)
			:SetHeight(20)
			:SetFont("BODY_BODY2_MEDIUM")
			:SetText(text)
			:SetScript("OnClick", callback)
		)
		numRows = numRows + 1
	end
	frame:SetHeight(12 + numRows * 20)
	self:ShowDialogFrame(frame)
end

--- Show a menu dialog.
-- @tparam ApplicationFrame self The application frame object
-- @tparam string frame The frame to anchor the dialog to
-- @tparam function iter A menu row iterator with the following fields: `index, text, subIter`
-- @param context Context to pass to the iter / subIter
-- @tparam function clickCallback The function to be called when a menu row is clicked
-- @tparam boolean flip Flip the anchor to the other side
function ApplicationFrame.ShowMenuDialog(self, frame, iter, context, clickCallback, flip)
	wipe(private.menuDialogContext)
	private.menuDialogContext.context = context
	private.menuDialogContext.clickCallback = clickCallback
	self:ShowDialogFrame(private.CreateMenuDialogFrame("_menuDialog", iter)
		:AddAnchor(flip and "TOPRIGHT" or "TOPLEFT", frame, flip and "BOTTOMRIGHT" or "BOTTOMLEFT", 2, -4)
	)
end

--- Hides the current dialog.
-- @tparam ApplicationFrame self The application frame object
function ApplicationFrame.HideDialog(self)
	local dialogFrame = tremove(self._dialogStack)
	if not dialogFrame then
		return
	end
	dialogFrame:GetParentElement():RemoveChild(dialogFrame)
	dialogFrame:Hide()
	dialogFrame:Release()
end

function ApplicationFrame.Draw(self)
	local frame = self:_GetBaseFrame()
	frame:SetToplevel(true)
	frame:Raise()
	self._nineSlice:SetVertexColor(Theme.GetColor("FRAME_BG"):GetFractionalRGBA())

	-- update the size if it's less than the set min size
	assert(self._minWidth > 0 and self._minHeight > 0)
	self._contextTable.width = max(self._contextTable.width, self._minWidth)
	self._contextTable.height = max(self._contextTable.height, self._minHeight)
	self._contextTable.scale = max(self._contextTable.scale, MIN_SCALE)

	-- set the frame size from the contextTable
	self:SetScale(self._contextTable.scale)
	self:SetSize(self._contextTable.width, self._contextTable.height)

	-- make sure at least 50px of the frame is on the screen and offset by at least 1 scaled pixel to fix some rendering issues
	local maxAbsCenterX = (UIParent:GetWidth() / self._contextTable.scale + self._contextTable.width) / 2 - MIN_ON_SCREEN_PX
	local maxAbsCenterY = (UIParent:GetHeight() / self._contextTable.scale + self._contextTable.height) / 2 - MIN_ON_SCREEN_PX
	local effectiveScale = UIParent:GetEffectiveScale()
	if self._contextTable.centerX < 0 then
		self._contextTable.centerX = min(max(self._contextTable.centerX, -maxAbsCenterX), -effectiveScale)
	else
		self._contextTable.centerX = max(min(self._contextTable.centerX, maxAbsCenterX), effectiveScale)
	end
	if self._contextTable.centerY < 0 then
		self._contextTable.centerY = min(max(self._contextTable.centerY, -maxAbsCenterY), -effectiveScale)
	else
		self._contextTable.centerY = max(min(self._contextTable.centerY, maxAbsCenterY), effectiveScale)
	end

	-- adjust the position of the frame based on the UI scale to make rendering more consistent
	self._contextTable.centerX = Math.Round(self._contextTable.centerX, effectiveScale)
	self._contextTable.centerY = Math.Round(self._contextTable.centerY, effectiveScale)

	-- set the frame position from the contextTable
	self:WipeAnchors()
	self:AddAnchor("CENTER", self._contextTable.centerX, self._contextTable.centerY)

	self.__super:Draw()
end



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

function ApplicationFrame._SavePositionAndSize(self, wasScaling)
	local frame = self:_GetBaseFrame()
	local parentFrame = frame:GetParent()
	local width = frame:GetWidth()
	local height = frame:GetHeight()
	if wasScaling then
		-- the anchor is in our old frame's scale, so convert the parent measurements to our old scale and then the resuslt to our new scale
		local scaleAdjustment = width / self._contextTable.width
		local frameLeftOffset = frame:GetLeft()  - parentFrame:GetLeft() / self._contextTable.scale
		self._contextTable.centerX = (frameLeftOffset - (parentFrame:GetWidth() / self._contextTable.scale - width) / 2) / scaleAdjustment
		local frameBottomOffset = frame:GetBottom() - parentFrame:GetBottom() / self._contextTable.scale
		self._contextTable.centerY = (frameBottomOffset - (parentFrame:GetHeight() / self._contextTable.scale - height) / 2) / scaleAdjustment
		self._contextTable.scale = self._contextTable.scale * scaleAdjustment
	else
		self._contextTable.width = width
		self._contextTable.height = height
		-- the anchor is in our frame's scale, so convert the parent measurements to our scale
		local frameLeftOffset = frame:GetLeft() - parentFrame:GetLeft() / self._contextTable.scale
		self._contextTable.centerX = (frameLeftOffset - (parentFrame:GetWidth() / self._contextTable.scale - width) / 2)
		local frameBottomOffset = frame:GetBottom() - parentFrame:GetBottom() / self._contextTable.scale
		self._contextTable.centerY = (frameBottomOffset - (parentFrame:GetHeight() / self._contextTable.scale - height) / 2)
	end
end

function ApplicationFrame._SetResizing(self, resizing)
	if resizing then
		self:GetElement("titleFrame"):Hide()
		self._contentFrame:_GetBaseFrame():SetAlpha(0)
		self._contentFrame:_GetBaseFrame():SetFrameStrata("LOW")
		self._contentFrame:Draw()
	else
		self:GetElement("titleFrame"):Show()
		self._contentFrame:_GetBaseFrame():SetAlpha(1)
		self._contentFrame:_GetBaseFrame():SetFrameStrata(self._strata)
	end
end



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

function private.CloseButtonOnClick(button)
	button:GetElement("__parent.__parent"):Hide()
end

function private.ResizeButtonOnEnter(self)
	local tooltip = L["Click and drag to resize this window."].."\n"..
		L["Hold SHIFT while dragging to scale the window instead."].."\n"..
		L["Right-Click to reset the window size, scale, and position to their defaults."]
	Tooltip.Show(self:_GetBaseFrame().resizeBtn, tooltip, true)
end

function private.ResizeButtonOnLeave(self)
	Tooltip.Hide()
end

function private.ResizeButtonOnMouseDown(self, mouseButton)
	if mouseButton ~= "LeftButton" then
		return
	end
	self._isScaling = IsShiftKeyDown()
	local frame = self:_GetBaseFrame()
	local width = frame:GetWidth()
	local height = frame:GetHeight()
	if self._isScaling then
		local minWidth = width * MIN_SCALE / self._contextTable.scale
		local minHeight = height * MIN_SCALE / self._contextTable.scale
		frame:SetMinResize(minWidth, minHeight)
		frame:SetMaxResize(width * 10, height * 10)
	else
		frame:SetMinResize(self._minWidth, self._minHeight)
		frame:SetMaxResize(width * 10, height * 10)
	end
	self:_SetResizing(true)
	frame:StartSizing("BOTTOMRIGHT")
	-- force updating the size here, to prevent using cached values from previously opened application frames
	frame:SetWidth(width)
	frame:SetHeight(height)
end

function private.ResizeButtonOnMouseUp(self, mouseButton)
	if mouseButton ~= "LeftButton" then
		return
	end
	self:_GetBaseFrame():StopMovingOrSizing()
	self:_SetResizing(false)
	self:_SavePositionAndSize(self._isScaling)
	self._isScaling = nil
	self:Draw()
end

function private.ResizeButtonOnClick(self, mouseButton)
	if mouseButton ~= "RightButton" then
		return
	end
	self._contextTable.scale = self._defaultContextTable.scale
	self._contextTable.width = self._defaultContextTable.width
	self._contextTable.height = self._defaultContextTable.height
	self._contextTable.centerX = self._defaultContextTable.centerX
	self._contextTable.centerY = self._defaultContextTable.centerY
	self:Draw()
end

function private.FrameOnDragStart(self)
	self:_GetBaseFrame():StartMoving()
end

function private.FrameOnDragStop(self)
	self:_GetBaseFrame():StopMovingOrSizing()
	self:_SavePositionAndSize()
	self:Draw()
end

function private.DialogOnMouseUp(dialog)
	local self = dialog:GetParentElement():GetParentElement()
	self:HideDialog()
end

function private.DialogOnHide(dialog)
	local context = dialog:GetContext()
	if context then
		TempTable.Release(context)
	end
end

function private.DialogCancelBtnOnClick(button)
	local self = button:GetBaseElement()
	self:HideDialog()
end

function private.DialogConfirmBtnOnClick(button)
	local self = button:GetBaseElement()
	local dialogFrame = button:GetParentElement():GetParentElement()
	local context = dialogFrame:GetContext()
	dialogFrame:SetContext(nil)
	self:HideDialog()
	context.callback(TempTable.UnpackAndRelease(context))
end

function private.CreateMenuDialogFrame(id, iter)
	local frame = UIElements.New("Frame", id)
		:SetLayout("VERTICAL")
		:SetWidth(180)
		:SetPadding(2)
		:SetBackgroundColor("PRIMARY_BG_ALT")
		:SetBorderColor("ACTIVE_BG_ALT")
	local numRows = 0
	for i, text, subIter in iter, private.menuDialogContext.context do
		frame:AddChild(UIElements.New("Frame", "row"..i)
			:SetLayout("HORIZONTAL")
			:SetHeight(21)
			:SetContext(subIter)
			:AddChild(UIElements.New("Button", "btn")
				:SetHeight(21)
				:SetPadding(8, 0, 0, 0)
				:SetFont("BODY_BODY3_MEDIUM")
				:SetBackground("PRIMARY_BG_ALT")
				:SetHighlightEnabled(true)
				:SetJustifyH("LEFT")
				:SetIcon(subIter and "iconPack.12x12/Chevron/Right" or nil, subIter and "RIGHT" or nil)
				:SetText(text)
				:SetContext(i)
				:SetScript("OnEnter", subIter and private.MenuDialogRowSubIterOnEnter or private.MenuDialogRowDefaultOnEnter)
				:SetScript("OnClick", not subIter and private.MenuDialogRowOnClick or nil)
			)
		)
		numRows = numRows + 1
	end
	frame:SetHeight(4 + numRows * 21)
	return frame
end

function private.MenuDialogRowOnClick(button)
	local path = TempTable.Acquire()
	tinsert(path, button:GetContext())
	local parentFrame = button:GetParentElement():GetParentElement():GetParentElement()
	local self = parentFrame:GetBaseElement()
	while parentFrame:GetParentElement() ~= self._contentFrame do
		local selectedButton = parentFrame:GetContext():GetElement("btn")
		tinsert(path, 1, selectedButton:GetContext())
		parentFrame = parentFrame:GetParentElement()
	end
	private.menuDialogContext.clickCallback(button, private.menuDialogContext.context, TempTable.UnpackAndRelease(path))
end

function private.MenuDialogRowDefaultOnEnter(button)
	local frame = button:GetParentElement():GetParentElement()
	if frame:HasChildById("subFrame") then
		local subFrame = frame:GetElement("subFrame")
		local prevRow = frame:GetContext()
		frame:SetContext(nil)
		if prevRow then
			prevRow:GetElement("btn"):SetHighlightLocked(false)
		end
		frame:RemoveChild(subFrame)
		subFrame:Release()
		frame:Draw()
	end
end

function private.MenuDialogRowSubIterOnEnter(button)
	private.MenuDialogRowDefaultOnEnter(button)
	button:SetHighlightLocked(true)
	local row = button:GetParentElement()
	local frame = row:GetParentElement()
	frame:SetContext(row)
	local subFrame = private.CreateMenuDialogFrame("subFrame", row:GetContext())
		:AddAnchor("TOPLEFT", button:_GetBaseFrame(), "TOPRIGHT", 4, 2)
	frame:AddChildNoLayout(subFrame)
	subFrame:Draw()
end

function private.GetAppStatusTooltip()
	local tooltipLines = TempTable.Acquire()
	tinsert(tooltipLines, format(L["TSM Desktop App Status (%s)"], TSM.GetRegion().."-"..GetRealmName()))

	local appUpdateAge = time() - TSM.GetAppUpdateTime()
	if appUpdateAge < 2 * SECONDS_PER_HOUR then
		tinsert(tooltipLines, Theme.GetFeedbackColor("GREEN"):ColorText(format(L["App Synced %s Ago"], SecondsToTime(appUpdateAge))))
	elseif appUpdateAge < 2 * SECONDS_PER_DAY then
		tinsert(tooltipLines, Theme.GetFeedbackColor("YELLOW"):ColorText(format(L["App Synced %s Ago"], SecondsToTime(appUpdateAge))))
	else
		tinsert(tooltipLines, Theme.GetFeedbackColor("RED"):ColorText(L["App Not Synced"]))
	end

	local auctionDBRealmTime, auctionDBRegionTime = TSM.AuctionDB.GetAppDataUpdateTimes()
	local auctionDBRealmAge = time() - auctionDBRealmTime
	local auctionDBRegionAge = time() - auctionDBRegionTime
	if auctionDBRealmAge < 4 * SECONDS_PER_HOUR then
		tinsert(tooltipLines, Theme.GetFeedbackColor("GREEN"):ColorText(format(L["AuctionDB Realm Data is %s Old"], SecondsToTime(auctionDBRealmAge))))
	elseif auctionDBRealmAge < 2 * SECONDS_PER_DAY then
		tinsert(tooltipLines, Theme.GetFeedbackColor("YELLOW"):ColorText(format(L["AuctionDB Realm Data is %s Old"], SecondsToTime(auctionDBRealmAge))))
	else
		tinsert(tooltipLines, Theme.GetFeedbackColor("RED"):ColorText(L["No AuctionDB Realm Data"]))
	end
	if auctionDBRegionAge < 2 * SECONDS_PER_DAY then
		tinsert(tooltipLines, Theme.GetFeedbackColor("GREEN"):ColorText(format(L["AuctionDB Region Data is %s Old"], SecondsToTime(auctionDBRegionAge))))
	else
		tinsert(tooltipLines, Theme.GetFeedbackColor("RED"):ColorText(L["No AuctionDB Region Data"]))
	end

	return strjoin("\n", TempTable.UnpackAndRelease(tooltipLines)), true, 16
end
