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

local _, TSM = ...
local Auctioning = TSM.UI.AuctionUI:NewPackage("Auctioning")
local L = TSM.Include("Locale").GetTable()
local FSM = TSM.Include("Util.FSM")
local Event = TSM.Include("Util.Event")
local Table = TSM.Include("Util.Table")
local Sound = TSM.Include("Util.Sound")
local Money = TSM.Include("Util.Money")
local Log = TSM.Include("Util.Log")
local Theme = TSM.Include("Util.Theme")
local ItemString = TSM.Include("Util.ItemString")
local Threading = TSM.Include("Service.Threading")
local ItemInfo = TSM.Include("Service.ItemInfo")
local CustomPrice = TSM.Include("Service.CustomPrice")
local BagTracking = TSM.Include("Service.BagTracking")
local AuctionTracking = TSM.Include("Service.AuctionTracking")
local AuctionScan = TSM.Include("Service.AuctionScan")
local Settings = TSM.Include("Service.Settings")
local UIElements = TSM.Include("UI.UIElements")
local private = {
	settings = nil,
	contentPath = "selection",
	hasLastScan = false,
	fsm = nil,
	scanContext = {},
	auctionScan = nil,
	groupSearch = "",
	selectionFrame = nil,
	logQuery = nil,
	perItem = true,
	canStartNewScan = false,
}
local SECONDS_PER_MIN = 60
local SECONDS_PER_HOUR = 60 * SECONDS_PER_MIN
local SECONDS_PER_DAY = 24 * SECONDS_PER_HOUR



-- ============================================================================
-- Module Functions
-- ============================================================================

function Auctioning.OnInitialize()
	private.settings = Settings.NewView()
		:AddKey("global", "auctionUIContext", "auctioningSelectionDividedContainer")
		:AddKey("global", "auctionUIContext", "auctioningSelectionVerticalDividedContainer")
		:AddKey("global", "auctionUIContext", "auctioningBagScrollingTable")
		:AddKey("global", "auctionUIContext", "auctioningLogScrollingTable")
		:AddKey("global", "auctionUIContext", "auctioningAuctionScrollingTable")
		:AddKey("global", "auctionUIContext", "auctioningTabGroup")
		:AddKey("char", "auctionUIContext", "auctioningGroupTree")
		:AddKey("global", "auctioningOptions", "scanCompleteSound")
		:AddKey("global", "auctioningOptions", "confirmCompleteSound")
	TSM.UI.AuctionUI.RegisterTopLevelPage(L["Auctioning"], private.GetAuctioningFrame, private.OnItemLinked)
	private.FSMCreate()
end



-- ============================================================================
-- Auctioning UI
-- ============================================================================

function private.GetAuctioningFrame()
	TSM.UI.AnalyticsRecordPathChange("auction", "auctioning")
	if not private.hasLastScan then
		private.contentPath = "selection"
	end
	return UIElements.New("ViewContainer", "auctioning")
		:SetNavCallback(private.GetAuctioningContentFrame)
		:AddPath("selection")
		:AddPath("scan")
		:SetPath(private.contentPath)
end

function private.GetAuctioningContentFrame(_, path)
	private.contentPath = path
	if path == "selection" then
		return private.GetAuctioningSelectionFrame()
	elseif path == "scan" then
		return private.GetAuctioningScanFrame()
	else
		error("Unexpected path: "..tostring(path))
	end
end

function private.GetAuctioningSelectionFrame()
	TSM.UI.AnalyticsRecordPathChange("auction", "auctioning", "selection")
	local frame = UIElements.New("DividedContainer", "selection")
		:SetSettingsContext(private.settings, "auctioningSelectionDividedContainer")
		:SetMinWidth(220, 250)
		:SetBackgroundColor("PRIMARY_BG")
		:SetLeftChild(UIElements.New("Frame", "groupSelection")
			:SetLayout("VERTICAL")
			:SetPadding(0, 0, 8, 0)
			:AddChild(UIElements.New("Frame", "title")
				:SetLayout("HORIZONTAL")
				:SetMargin(0, 0, 0, 8)
				:SetHeight(24)
				:AddChild(UIElements.New("Input", "search")
					:SetMargin(8, 8, 0, 0)
					:SetIconTexture("iconPack.18x18/Search")
					:SetClearButtonEnabled(true)
					:AllowItemInsert(true)
					:SetHintText(L["Search Groups"])
					:SetValue(private.groupSearch)
					:SetScript("OnValueChanged", private.GroupSearchOnValueChanged)
				)
				:AddChild(UIElements.New("Button", "expandAllBtn")
					:SetSize(24, 24)
					:SetMargin(0, 4, 0, 0)
					:SetBackground("iconPack.18x18/Expand All")
					:SetScript("OnClick", private.ExpandAllGroupsOnClick)
					:SetTooltip(L["Expand / Collapse All Groups"])
				)
				:AddChild(UIElements.New("Button", "selectAllBtn")
					:SetSize(24, 24)
					:SetMargin(0, 8, 0, 0)
					:SetBackground("iconPack.18x18/Select All")
					:SetScript("OnClick", private.SelectAllGroupsOnClick)
					:SetTooltip(L["Select / Deselect All Groups"])
				)
			)
			:AddChild(UIElements.New("Texture", "line")
				:SetHeight(2)
				:SetTexture("ACTIVE_BG")
			)
			:AddChild(UIElements.New("ApplicationGroupTree", "groupTree")
				:SetSettingsContext(private.settings, "auctioningGroupTree")
				:SetQuery(TSM.Groups.CreateQuery(), "Auctioning")
				:SetSearchString(private.groupSearch)
				:SetScript("OnGroupSelectionChanged", private.GroupTreeOnGroupSelectionChanged)
			)
			:AddChild(UIElements.New("Frame", "bottom")
				:SetLayout("VERTICAL")
				:SetHeight(74)
				:SetBackgroundColor("PRIMARY_BG_ALT")
				:AddChild(UIElements.New("Texture", "line")
					:SetHeight(2)
					:SetTexture("ACTIVE_BG")
				)
				:AddChild(UIElements.New("ActionButton", "postScanBtn")
					:SetHeight(24)
					:SetMargin(8)
					:SetText(L["Run Post Scan"])
					:SetScript("OnClick", private.RunPostButtonOnclick)
				)
				:AddChild(UIElements.New("ActionButton", "cancelScanBtn")
					:SetHeight(24)
					:SetMargin(8, 8, 0, 8)
					:SetText(L["Run Cancel Scan"])
					:SetScript("OnClick", private.RunCancelButtonOnclick)
				)
			)
		)
		:SetRightChild(UIElements.New("DividedContainer", "content")
			:SetVertical()
			:SetMargin(0, 0, 6, 0)
			:SetSettingsContext(private.settings, "auctioningSelectionVerticalDividedContainer")
			:SetMinWidth(50, 100)
			:SetBackgroundColor("PRIMARY_BG")
			:SetTopChild(UIElements.New("Frame", "content")
				:SetLayout("VERTICAL")
				:AddChild(UIElements.New("TabGroup", "buttons")
					:SetNavCallback(private.GetScansElement)
					:SetSettingsContext(private.settings, "auctioningTabGroup")
					:AddPath(L["Recent Scans"])
					:AddPath(L["Favorite Scans"])
				)
			)
			:SetBottomChild(UIElements.New("Frame", "content")
				:SetLayout("VERTICAL")
				:AddChild(UIElements.New("Frame", "bottom")
					:SetLayout("VERTICAL")
					:SetHeight(37)
					:SetBackgroundColor("PRIMARY_BG_ALT")
					:AddChild(UIElements.New("Text", "label")
						:SetHeight(24)
						:SetMargin(4, 0, 6, 6)
						:SetFont("BODY_BODY1_BOLD")
						:SetText(L["Post Items from Bags"])
					)
				)
				:AddChild(UIElements.New("SelectionScrollingTable", "bagScrollingTable")
					:SetSettingsContext(private.settings, "auctioningBagScrollingTable")
					:GetScrollingTableInfo()
						:NewColumn("item")
							:SetTitle(L["Item"])
							:SetHeaderIndent(18)
							:SetFont("ITEM_BODY3")
							:SetJustifyH("LEFT")
							:SetIconSize(12)
							:SetTextInfo("autoBaseItemString", TSM.UI.GetColoredItemName)
							:SetIconInfo("itemTexture")
							:SetTooltipInfo("autoBaseItemString")
							:SetSortInfo("name")
							:DisableHiding()
							:Commit()
						:NewColumn("operation")
							:SetTitle(L["Auctioning Operation"])
							:SetFont("BODY_BODY3_MEDIUM")
							:SetJustifyH("LEFT")
							:SetTextInfo("firstOperation", private.BagGetOperationText)
							:SetSortInfo("firstOperation")
							:Commit()
						:Commit()
					:SetQuery(TSM.Auctioning.PostScan.CreateBagsQuery())
					:SetAutoReleaseQuery(true)
					:SetSelectionValidator(private.BagScrollingTableIsSelectionEnabled)
					:SetScript("OnSelectionChanged", private.BagOnSelectionChanged)
				)
				:AddChild(UIElements.New("Texture", "line")
					:SetHeight(2)
					:SetTexture("ACTIVE_BG")
				)
				:AddChild(UIElements.New("Frame", "button")
					:SetLayout("HORIZONTAL")
					:SetHeight(40)
					:SetPadding(8)
					:SetBackgroundColor("PRIMARY_BG_ALT")
					:AddChild(UIElements.New("ActionButton", "postSelected")
						:SetHeight(24)
						:SetMargin(0, 8, 0, 0)
						:SetDisabled(true)
						:SetText(L["Post Selected"])
						:SetScript("OnClick", private.RunPostBagsButtonOnClick)
					)
					:AddChild(UIElements.New("Button", "selectAll")
						:SetSize("AUTO", 20)
						:SetMargin(0, 8, 0, 0)
						:SetFont("BODY_BODY3_MEDIUM")
						:SetText(L["Select All"])
						:SetScript("OnClick", private.SelectAllOnClick)
					)
					:AddChild(UIElements.New("Texture", "line")
						:SetSize(2, 20)
						:SetMargin(0, 8, 0, 0)
						:SetTexture("ACTIVE_BG")
					)
					:AddChild(UIElements.New("Button", "clearAll")
						:SetSize("AUTO", 20)
						:SetFont("BODY_BODY3_MEDIUM")
						:SetText(L["Clear All"])
						:SetDisabled(true)
						:SetScript("OnClick", private.ClearAllOnClick)
					)
				)
			)
		)
		:SetScript("OnHide", private.SelectionOnHide)
	local noGroupSelected = frame:GetElement("groupSelection.groupTree"):IsSelectionCleared(true)
	frame:GetElement("groupSelection.bottom.postScanBtn"):SetDisabled(noGroupSelected)
	frame:GetElement("groupSelection.bottom.cancelScanBtn"):SetDisabled(noGroupSelected)
	private.selectionFrame = frame
	return frame
end

function private.GetScansElement(_, button)
	if button == L["Recent Scans"] then
		return UIElements.New("SearchList", "list")
			:SetQuery(TSM.Auctioning.SavedSearches.CreateRecentSearchesQuery())
			:SetEditButtonHidden(true)
			:SetScript("OnFavoriteChanged", private.SearchListOnFavoriteChanged)
			:SetScript("OnDelete", private.SearchListOnDelete)
			:SetScript("OnRowClick", private.SearchListOnRowClick)
	elseif button == L["Favorite Scans"] then
		return UIElements.New("SearchList", "list")
			:SetQuery(TSM.Auctioning.SavedSearches.CreateFavoriteSearchesQuery())
			:SetScript("OnFavoriteChanged", private.SearchListOnFavoriteChanged)
			:SetScript("OnEditClick", private.SearchListOnEditClick)
			:SetScript("OnDelete", private.SearchListOnDelete)
			:SetScript("OnRowClick", private.SearchListOnRowClick)
	else
		error("Unexpected button: "..tostring(button))
	end
end

function private.GetAuctioningScanFrame()
	TSM.UI.AnalyticsRecordPathChange("auction", "auctioning", "scan")
	return UIElements.New("Frame", "scan")
		:SetLayout("VERTICAL")
		:SetBackgroundColor("PRIMARY_BG")
		:AddChild(UIElements.New("Frame", "header")
			:SetLayout("HORIZONTAL")
			:SetPadding(8)
			:SetHeight(92)
			:AddChild(UIElements.New("Frame", "back")
				:SetLayout("VERTICAL")
				:SetWidth(54)
				:SetHeight(76)
				:SetMargin(0, 8, 0, 0)
				:SetPadding(0, 0, 4, 0)
				:SetBackgroundColor("PRIMARY_BG_ALT", true)
				:AddChild(UIElements.New("Button", "backBtn")
					:SetMargin(0, 0, 2, 0)
					:SetSize(28, 28)
					:SetBackground("iconPack.24x24/Close/Default")
					:SetScript("OnEnter", private.ExitScanButtonOnEnter)
					:SetScript("OnLeave", private.ExitScanButtonOnLeave)
					:SetScript("OnClick", private.ExitScanButtonOnClick)
				)
				:AddChild(UIElements.New("Text", "text")
					:SetHeight(18)
					:SetFont("BODY_BODY3_MEDIUM")
					:SetJustifyH("CENTER")
					:SetText(L["Exit"])
				)
				:AddChild(UIElements.New("Text", "text")
					:SetHeight(18)
					:SetFont("BODY_BODY3_MEDIUM")
					:SetJustifyH("CENTER")
					:SetText(L["Scan"])
				)
				:SetScript("OnEnter", private.ExitScanFrameOnEnter)
				:SetScript("OnLeave", private.ExitScanFrameOnLeave)
				:SetScript("OnMouseUp", private.ExitScanButtonOnClick)
			)
			:AddChild(UIElements.New("Frame", "content")
				:SetLayout("HORIZONTAL")
				:SetPadding(8, 8, 4, 4)
				:SetBackgroundColor("PRIMARY_BG_ALT", true)
				:AddChild(UIElements.New("Frame", "item")
					:SetLayout("VERTICAL")
					:AddChild(UIElements.New("Frame", "content")
						:SetLayout("HORIZONTAL")
						:SetHeight(24)
						:SetMargin(0, 0, 0, 4)
						:AddChild(UIElements.New("Button", "icon")
							:SetSize(18, 18)
						)
						:AddChild(UIElements.New("Button", "text")
							:SetMargin(4, 0, 0, 0)
							:SetFont("ITEM_BODY1")
							:SetJustifyH("LEFT")
						)
					)
					:AddChild(UIElements.New("Frame", "cost")
						:SetLayout("HORIZONTAL")
						:SetHeight(20)
						:AddChild(UIElements.New("Text", "desc")
							:SetWidth("AUTO")
							:SetFont("BODY_BODY3_MEDIUM")
							:SetText(L["Deposit Cost"]..":")
						)
						:AddChild(UIElements.New("Text", "text")
							:SetMargin(4, 0, 0, 0)
							:SetFont("TABLE_TABLE1")
						)
					)
					:AddChild(UIElements.New("Frame", "operation")
						:SetLayout("HORIZONTAL")
						:SetHeight(20)
						:AddChild(UIElements.New("Text", "desc")
							:SetWidth("AUTO")
							:SetFont("BODY_BODY3_MEDIUM")
							:SetText(L["Operation"]..":")
						)
						:AddChild(UIElements.New("Text", "text")
							:SetMargin(4, 0, 0, 0)
							:SetFont("BODY_BODY3_MEDIUM")
						)
					)
				)
				:AddChild(UIElements.New("Frame", "details")
					:SetLayout("VERTICAL")
					:SetWidth(371)
					:SetMargin(16, 0, 0, 0)
					:AddChild(UIElements.New("Frame", "header")
						:SetLayout("HORIZONTAL")
						:SetHeight(24)
						:AddChild(UIElements.New("Text", "text")
							:SetMargin(0, 10, 0, 0)
							:SetSize("AUTO", 16)
							:SetFont("BODY_BODY1")
							:SetText(L["Auctioning Details"])
						)
						:AddChild(UIElements.New("ActionButton", "editBtn")
							:SetHeight(16)
							:SetFont("BODY_BODY3_MEDIUM")
							:SetText(L["Edit Post"])
							:SetScript("OnClick", private.EditButtonOnClick)
						)
					)
					:AddChild(UIElements.New("Frame", "details")
						:SetLayout("HORIZONTAL")
						:SetMargin(0, 0, 4, 0)
						:AddChild(UIElements.New("Frame", "details1")
							:SetLayout("VERTICAL")
							:SetWidth(200)
							:SetMargin(0, 8, 0, 0)
							:AddChild(UIElements.New("Frame", "bid")
								:SetLayout("HORIZONTAL")
								:SetHeight(20)
								:AddChild(UIElements.New("Text", "desc")
									:SetWidth("AUTO")
									:SetFont("BODY_BODY3_MEDIUM")
									:SetText(L["Bid Price"]..":")
								)
								:AddChild(UIElements.New("Text", "text")
									:SetMargin(4, 0, 0, 0)
									:SetFont("TABLE_TABLE1")
								)
							)
							:AddChild(UIElements.New("Frame", "buyout")
								:SetLayout("HORIZONTAL")
								:SetHeight(20)
								:AddChild(UIElements.New("Text", "desc")
									:SetWidth("AUTO")
									:SetFont("BODY_BODY3_MEDIUM")
									:SetText(L["Buyout Price"]..":")
								)
								:AddChild(UIElements.New("Text", "text")
									:SetMargin(4, 0, 0, 0)
									:SetFont("TABLE_TABLE1")
								)
							)
						)
						:AddChild(UIElements.New("Frame", "details2")
							:SetLayout("VERTICAL")
							:AddChild(UIElements.New("Frame", "quantity")
								:SetLayout("HORIZONTAL")
								:SetHeight(20)
								:AddChild(UIElements.New("Text", "desc")
									:SetWidth("AUTO")
									:SetFont("BODY_BODY3_MEDIUM")
									:SetText((TSM.IsWowClassic() and L["Stack / Quantity"] or L["Quantity"])..":")
								)
								:AddChild(UIElements.New("Text", "text")
									:SetMargin(4, 0, 0, 0)
									:SetFont("TABLE_TABLE1")
								)
							)
							:AddChild(UIElements.New("Frame", "duration")
								:SetLayout("HORIZONTAL")
								:SetHeight(20)
								:AddChild(UIElements.New("Text", "desc")
									:SetWidth("AUTO")
									:SetFont("BODY_BODY3_MEDIUM")
									:SetText(L["Duration"]..":")
								)
								:AddChild(UIElements.New("Text", "text")
									:SetMargin(4, 0, 0, 0)
									:SetFont("TABLE_TABLE1")
								)
							)
						)
					)
				)
			)
		)
		:AddChild(UIElements.New("SimpleTabGroup", "tabs")
			:SetNavCallback(private.ScanNavCallback)
			:AddPath(L["Auctioning Log"], true)
			:AddPath(L["All Auctions"])
		)
		:AddChild(UIElements.New("Texture", "line")
			:SetHeight(2)
			:SetTexture("ACTIVE_BG")
		)
		:AddChild(UIElements.New("Frame", "bottom")
			:SetLayout("HORIZONTAL")
			:SetHeight(40)
			:SetPadding(8)
			:SetBackgroundColor("PRIMARY_BG_ALT")
			:AddChild(UIElements.New("ActionButton", "pauseResumeBtn")
				:SetSize(24, 24)
				:SetMargin(0, 8, 0, 0)
				:SetIcon("iconPack.18x18/PlayPause")
				:SetScript("OnClick", private.PauseResumeBtnOnClick)
			)
			:AddChild(UIElements.New("ProgressBar", "progressBar")
				:SetHeight(24)
				:SetMargin(0, 8, 0, 0)
				:SetProgress(0)
				:SetProgressIconHidden(false)
				:SetText(L["Starting Scan..."])
			)
			:AddChild(UIElements.NewNamed("ActionButton", "processBtn", "TSMAuctioningBtn")
				:SetSize(160, 24)
				:SetMargin(0, 8, 0, 0)
				:SetText(L["Post"])
				:SetDisabled(true)
				:DisableClickCooldown(true)
				:SetScript("OnClick", private.ProcessButtonOnClick)
			)
			:AddChild(UIElements.New("ActionButton", "skipBtn")
				:SetSize(160, 24)
				:SetText(L["Skip"])
				:SetDisabled(true)
				:DisableClickCooldown(true)
				:SetScript("OnClick", private.SkipButtonOnClick)
			)
		)
		:SetScript("OnUpdate", private.ScanFrameOnUpdate)
		:SetScript("OnHide", private.ScanFrameOnHide)
end

function private.ScanNavCallback(_, path)
	if path == L["Auctioning Log"] then
		TSM.UI.AnalyticsRecordPathChange("auction", "auctioning", "scan", "log")
		private.logQuery = private.logQuery or TSM.Auctioning.Log.CreateQuery()
		return UIElements.New("Frame", "logFrame")
			:SetLayout("VERTICAL")
			:AddChild(UIElements.New("QueryScrollingTable", "log")
				:SetSettingsContext(private.settings, "auctioningLogScrollingTable")
				:GetScrollingTableInfo()
					:NewColumn("index")
						:SetTitleIcon("iconPack.14x14/Attention")
						:SetIconSize(12)
						:SetFont("ITEM_BODY3")
						:SetJustifyH("CENTER")
						:SetIconInfo(nil, private.LogGetIndexIcon)
						:SetSortInfo("index")
						:Commit()
					:NewColumn("item")
						:SetTitle(L["Item"])
						:SetFont("ITEM_BODY3")
						:SetJustifyH("LEFT")
						:SetIconSize(12)
						:SetTextInfo("itemString", TSM.UI.GetColoredItemName)
						:SetIconInfo("itemString", ItemInfo.GetTexture)
						:SetTooltipInfo("itemString")
						:SetSortInfo("name")
						:Commit()
					:NewColumn("buyout")
						:SetTitle(L["Your Buyout"])
						:SetFont("TABLE_TABLE1")
						:SetJustifyH("RIGHT")
						:SetTextInfo("buyout", private.LogGetBuyoutText)
						:SetSortInfo("buyout")
						:Commit()
					:NewColumn("operation")
						:SetTitle(L["Operation"])
						:SetFont("TABLE_TABLE1")
						:SetJustifyH("LEFT")
						:SetTextInfo("operation")
						:SetSortInfo("operation")
						:Commit()
					:NewColumn("seller")
						:SetTitle(L["Seller"])
						:SetFont("TABLE_TABLE1")
						:SetJustifyH("LEFT")
						:SetTextInfo("seller")
						:SetSortInfo("seller")
						:Commit()
					:NewColumn("info")
						:SetTitle(INFO)
						:SetFont("TABLE_TABLE1")
						:SetJustifyH("LEFT")
						:SetTextInfo(nil, TSM.Auctioning.Log.GetInfoStr)
						:SetSortInfo("reasonStr")
						:Commit()
					:Commit()
				:SetQuery(private.logQuery)
				:SetSelectionDisabled(true)
			)
	elseif path == L["All Auctions"] then
		TSM.UI.AnalyticsRecordPathChange("auction", "auctioning", "scan", "auctions")
		return UIElements.New("Frame", "auctionsFrame")
			:SetLayout("VERTICAL")
			:AddChild(UIElements.New("AuctionScrollingTable", "auctions")
				:SetSettingsContext(private.settings, "auctioningAuctionScrollingTable")
				:SetBrowseResultsVisible(true)
				:SetMarketValueFunction(private.MarketValueFunction)
				:SetAuctionScan(private.auctionScan)
			)
	else
		error("Unexpected path: "..tostring(path))
	end
end



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

function private.OnItemLinked(_, itemLink)
	if private.selectionFrame then
		if not TSM.UI.AuctionUI.StartingScan(L["Auctioning"]) then
			return false
		end
		wipe(private.scanContext)
		private.scanContext.isItems = true
		tinsert(private.scanContext, TSM.Groups.TranslateItemString(ItemString.Get(itemLink)))
		private.selectionFrame:GetParentElement():SetPath("scan", true)
		private.fsm:ProcessEvent("EV_START_SCAN", "POST", private.scanContext)
		return true
	else
		if not private.canStartNewScan then
			return false
		end
		wipe(private.scanContext)
		private.scanContext.isItems = true
		tinsert(private.scanContext, TSM.Groups.TranslateItemString(ItemString.Get(itemLink)))
		private.fsm:ProcessEvent("EV_START_SCAN", "POST", private.scanContext)
		return true
	end
end

function private.SelectionOnHide(frame)
	assert(frame == private.selectionFrame)
	private.selectionFrame = nil
end

function private.GroupSearchOnValueChanged(input)
	private.groupSearch = strlower(input:GetValue())
	input:GetElement("__parent.__parent.groupTree")
		:SetSearchString(private.groupSearch)
		:Draw()
end

function private.ExpandAllGroupsOnClick(button)
	button:GetElement("__parent.__parent.groupTree")
		:ToggleExpandAll()
end

function private.SelectAllGroupsOnClick(button)
	button:GetElement("__parent.__parent.groupTree")
		:ToggleSelectAll()
end

function private.GroupTreeOnGroupSelectionChanged(groupTree)
	local postScanBtn = groupTree:GetElement("__parent.bottom.postScanBtn")
	postScanBtn:SetDisabled(groupTree:IsSelectionCleared())
	postScanBtn:Draw()
	local cancelScanBtn = groupTree:GetElement("__parent.bottom.cancelScanBtn")
	cancelScanBtn:SetDisabled(groupTree:IsSelectionCleared())
	cancelScanBtn:Draw()
end

function private.RunPostButtonOnclick(button)
	if not TSM.UI.AuctionUI.StartingScan(L["Auctioning"]) then
		return
	end
	wipe(private.scanContext)
	for _, groupPath in button:GetElement("__parent.__parent.groupTree"):SelectedGroupsIterator() do
		tinsert(private.scanContext, groupPath)
	end
	button:GetElement("__parent.__parent.__parent.__parent"):SetPath("scan", true)
	private.fsm:ProcessEvent("EV_START_SCAN", "POST", private.scanContext)
end

function private.RunCancelButtonOnclick(button)
	if not TSM.UI.AuctionUI.StartingScan(L["Auctioning"]) then
		return
	end
	wipe(private.scanContext)
	for _, groupPath in button:GetElement("__parent.__parent.groupTree"):SelectedGroupsIterator() do
		tinsert(private.scanContext, groupPath)
	end
	button:GetElement("__parent.__parent.__parent.__parent"):SetPath("scan", true)
	private.fsm:ProcessEvent("EV_START_SCAN", "CANCEL", private.scanContext)
end

function private.SearchListOnFavoriteChanged(_, dbRow, isFavorite)
	TSM.Auctioning.SavedSearches.SetSearchIsFavorite(dbRow, isFavorite)
end

function private.SearchListOnEditClick(searchList, dbRow)
	local dialog = UIElements.New("Frame", "frame")
		:SetLayout("VERTICAL")
		:SetSize(600, 187)
		:AddAnchor("CENTER")
		:SetBackgroundColor("FRAME_BG")
		:SetBorderColor("ACTIVE_BG")
		:AddChild(UIElements.New("Text", "title")
			:SetHeight(44)
			:SetMargin(16, 16, 24, 16)
			:SetFont("BODY_BODY1_BOLD")
			:SetJustifyH("CENTER")
			:SetText(L["Rename Search"])
		)
		:AddChild(UIElements.New("Input", "nameInput")
			:SetHeight(26)
			:SetMargin(16, 16, 0, 25)
			:SetBackgroundColor("PRIMARY_BG_ALT")
			:AllowItemInsert(true)
			:SetContext(dbRow)
			:SetValue(dbRow:GetField("name"))
			:SetScript("OnEnterPressed", private.RenameInputOnEnterPressed)
		)
		:AddChild(UIElements.New("Frame", "buttons")
			:SetLayout("HORIZONTAL")
			:SetMargin(16, 16, 0, 16)
			:AddChild(UIElements.New("Spacer", "spacer"))
			:AddChild(UIElements.New("ActionButton", "closeBtn")
				:SetSize(126, 26)
				:SetText(CLOSE)
				:SetScript("OnClick", private.DialogCloseBtnOnClick)
			)
		)
	searchList:GetBaseElement():ShowDialogFrame(dialog)
	dialog:GetElement("nameInput"):SetFocused(true)
end

function private.RenameInputOnEnterPressed(input)
	local name = input:GetValue()
	if name == "" then
		return
	end
	local dbRow = input:GetContext()
	local baseElement = input:GetBaseElement()
	baseElement:HideDialog()
	TSM.Auctioning.SavedSearches.RenameSearch(dbRow, name)
end

function private.DialogCloseBtnOnClick(button)
	button:GetBaseElement():HideDialog()
end

function private.SearchListOnDelete(_, dbRow)
	TSM.Auctioning.SavedSearches.DeleteSearch(dbRow)
end

function private.SearchListOnRowClick(searchList, dbRow)
	if not TSM.UI.AuctionUI.StartingScan(L["Auctioning"]) then
		return
	end
	local scanType = dbRow:GetField("searchType")
	wipe(private.scanContext)
	private.scanContext.isItems = scanType == "postItems" or nil
	TSM.Auctioning.SavedSearches.FiltersToTable(dbRow, private.scanContext)
	searchList:GetParentElement():GetParentElement():GetParentElement():GetParentElement():GetParentElement():SetPath("scan", true)
	private.fsm:ProcessEvent("EV_START_SCAN", scanType == "cancelGroups" and "CANCEL" or "POST", private.scanContext)
end

function private.PauseResumeBtnOnClick(button)
	private.fsm:ProcessEvent("EV_PAUSE_RESUME_CLICKED")
end

function private.ProcessButtonOnClick(button)
	private.fsm:ProcessEvent("EV_PROCESS_CLICKED")
end

function private.SkipButtonOnClick(button)
	private.fsm:ProcessEvent("EV_SKIP_CLICKED")
end

function private.ScanFrameOnUpdate(frame)
	frame:SetScript("OnUpdate", nil)
	private.fsm:ProcessEvent("EV_SCAN_FRAME_SHOWN", frame)
end

function private.ScanFrameOnHide(frame)
	private.fsm:ProcessEvent("EV_SCAN_FRAME_HIDDEN")
end

function private.BagOnSelectionChanged(scrollingTable)
	local selectionCleared = scrollingTable:IsSelectionCleared()
	scrollingTable:GetElement("__parent.button.postSelected"):SetDisabled(selectionCleared)
		:Draw()
	scrollingTable:GetElement("__parent.button.selectAll"):SetDisabled(scrollingTable:IsAllSelected())
		:Draw()
	scrollingTable:GetElement("__parent.button.clearAll"):SetDisabled(selectionCleared)
		:Draw()
end

function private.SelectAllOnClick(button)
	button:GetElement("__parent.__parent.bagScrollingTable"):SelectAll()
end

function private.ClearAllOnClick(button)
	button:GetElement("__parent.__parent.bagScrollingTable"):ClearSelection()
end

function private.RunPostBagsButtonOnClick(button)
	if not TSM.UI.AuctionUI.StartingScan(L["Auctioning"]) then
		return
	end
	wipe(private.scanContext)
	private.scanContext.isItems = true
	for _, row in button:GetElement("__parent.__parent.bagScrollingTable"):SelectionIterator() do
		local autoBaseItemString, operation = row:GetFields("autoBaseItemString", "firstOperation")
		if operation then
			tinsert(private.scanContext, autoBaseItemString)
		end
	end
	button:GetParentElement():GetParentElement():GetParentElement():GetParentElement():GetParentElement():SetPath("scan", true)
	private.fsm:ProcessEvent("EV_START_SCAN", "POST", private.scanContext)
end

function private.ExitScanButtonOnEnter(button)
	private.ExitScanFrameOnEnter(button:GetParentElement())
end

function private.ExitScanButtonOnLeave(button)
	private.ExitScanFrameOnLeave(button:GetParentElement())
end

function private.ExitScanFrameOnEnter(frame)
	frame:SetBackgroundColor("PRIMARY_BG_ALT+HOVER", true)
		:Draw()
end

function private.ExitScanFrameOnLeave(frame)
	frame:SetBackgroundColor("PRIMARY_BG_ALT", true)
		:Draw()
end

function private.ExitScanButtonOnClick()
	if TSM.IsWowClassic() then
		ClearCursor()
		ClickAuctionSellItemButton(AuctionsItemButton, "LeftButton")
		ClearCursor()
	end
	private.fsm:ProcessEvent("EV_BACK_BUTTON_CLICKED")
end

function private.EditButtonOnClick(button)
	private.fsm:ProcessEvent("EV_EDIT_BUTTON_CLICKED")
end



-- ============================================================================
-- FSM
-- ============================================================================

function private.FSMCreate()
	local fsmContext = {
		itemString = nil,
		scanFrame = nil,
		scanThreadId = nil,
		scanType = nil,
		auctionScan = nil,
		pausePending = nil,
		isScanning = false,
		pendingFuture = nil,
	}
	Event.Register("AUCTION_HOUSE_CLOSED", function()
		private.fsm:ProcessEvent("EV_AUCTION_HOUSE_CLOSED")
	end)
	if TSM.IsWowClassic() then
		Event.Register("CHAT_MSG_SYSTEM", function(_, msg)
			if msg == ERR_AUCTION_STARTED then
				private.fsm:SetLoggingEnabled(false)
				private.fsm:ProcessEvent("EV_AUCTION_POST_CONFIRM", true)
				private.fsm:SetLoggingEnabled(true)
			elseif msg == ERR_AUCTION_REMOVED then
				private.fsm:SetLoggingEnabled(false)
				private.fsm:ProcessEvent("EV_AUCTION_CANCEL_CONFIRM", true)
				private.fsm:SetLoggingEnabled(true)
			end
		end)
		local POST_ERR_MSGS = {
			-- errors where we can retry
			[ERR_ITEM_NOT_FOUND] = true,
			[ERR_AUCTION_DATABASE_ERROR] = true,
			-- errors where we can't retry
			[ERR_AUCTION_REPAIR_ITEM] = false,
			[ERR_AUCTION_LIMITED_DURATION_ITEM] = false,
			[ERR_AUCTION_USED_CHARGES] = false,
			[ERR_AUCTION_WRAPPED_ITEM] = false,
			[ERR_AUCTION_BAG] = false,
			[ERR_NOT_ENOUGH_MONEY] = false,
		}
		Event.Register("UI_ERROR_MESSAGE", function(_, _, msg)
			if POST_ERR_MSGS[msg] ~= nil then
				private.fsm:ProcessEvent("EV_AUCTION_POST_CONFIRM", false, POST_ERR_MSGS[msg])
			end
			if msg == ERR_ITEM_NOT_FOUND then
				private.fsm:ProcessEvent("EV_AUCTION_CANCEL_CONFIRM", false, true)
			end
		end)
	end
	local fsmPrivate = {}
	function fsmPrivate.UpdateDepositCost(context)
		if context.scanType ~= "POST" then
			return
		end

		local header = context.scanFrame:GetElement("header")
		local detailsHeader1 = header:GetElement("content.details.details.details1")

		local currentRow = TSM.Auctioning.PostScan.GetCurrentRow()
		if not currentRow then
			return
		end

		local itemString = currentRow:GetField("itemString")
		local postBag, postSlot = BagTracking.CreateQueryBagsAuctionable()
			:OrderBy("slotId", true)
			:Select("bag", "slot")
			:Equal("baseItemString", ItemString.GetBaseFast(itemString))
			:VirtualField("autoBaseItemString", "string", TSM.Groups.TranslateItemString, "itemString")
			:Equal("autoBaseItemString", itemString)
			:GetFirstResultAndRelease()
		local postTime = currentRow:GetField("postTime")
		local stackSize = tonumber(currentRow:GetField("stackSize"))
		local depositCost = 0
		if postBag and postSlot then
			if TSM.IsWowClassic() then
				ClearCursor()
				PickupContainerItem(postBag, postSlot)
				ClickAuctionSellItemButton(AuctionsItemButton, "LeftButton")
				ClearCursor()
				local bid = Money.FromString(detailsHeader1:GetElement("bid.text"):GetText())
				local buyout = Money.FromString(detailsHeader1:GetElement("buyout.text"):GetText())
				depositCost = GetAuctionDeposit(postTime, bid, buyout, stackSize, 1)
				ClearCursor()
				ClickAuctionSellItemButton(AuctionsItemButton, "LeftButton")
				ClearCursor()
			else
				local isCommodity = ItemInfo.IsCommodity(itemString)
				depositCost = max(floor(0.15 * (ItemInfo.GetVendorSell(itemString) or 0) * (isCommodity and stackSize or 1) * (postTime == 3 and 4 or postTime)), 100) * (isCommodity and 1 or stackSize)
			end
		end

		header:GetElement("content.item.cost.text"):SetText(Money.ToString(depositCost))
			:Draw()
	end
	function fsmPrivate.UpdateScanFrame(context)
		if not context.scanFrame then
			return
		end

		local header = context.scanFrame:GetElement("header")
		local currentRow, numProcessed, numConfirmed, _, totalNum = nil, nil, nil, nil, nil
		if context.scanType == "POST" then
			currentRow = TSM.Auctioning.PostScan.GetCurrentRow()
			numProcessed, numConfirmed, _, totalNum = TSM.Auctioning.PostScan.GetStatus()
			header:GetElement("content.item.cost")
				:Show()
				:Draw()
		elseif context.scanType == "CANCEL" then
			currentRow = TSM.Auctioning.CancelScan.GetCurrentRow()
			numProcessed, numConfirmed, _, totalNum = TSM.Auctioning.CancelScan.GetStatus()
			header:GetElement("content.item.cost")
				:Hide()
				:Draw()
		else
			error("Invalid scan type: "..tostring(context.scanType))
		end
		local itemContent = header:GetElement("content.item.content")
		local detailsHeader1 = header:GetElement("content.details.details.details1")
		local detailsHeader2 = header:GetElement("content.details.details.details2")
		if currentRow then
			local selectedRow = nil
			for _, row in private.logQuery:Iterator() do
				if currentRow:GetField("auctionId") == row:GetField("index") then
					selectedRow = row
				end
			end
			if selectedRow and context.scanFrame:GetElement("tabs"):GetPath() == L["Auctioning Log"] then
				context.scanFrame:GetElement("tabs.logFrame.log")
					:SetSelection(selectedRow:GetUUID())
					:Draw()
			end

			local itemString = currentRow:GetField("itemString")
			local rowStacksRemaining = currentRow:GetField("numStacks") - currentRow:GetField("numProcessed")
			itemContent:GetElement("icon")
				:SetBackground(ItemInfo.GetTexture(itemString))
				:SetTooltip(itemString)
				:Draw()
			itemContent:GetElement("text")
				:SetText(TSM.UI.GetColoredItemName(itemString))
				:SetTooltip(itemString)
				:Draw()
			header:GetElement("content.item.operation.text")
				:SetText(currentRow:GetField("operationName"))
				:Draw()
			detailsHeader1:GetElement("bid.text")
				:SetText(Money.ToString(currentRow:GetField(ItemInfo.IsCommodity(itemString) and "itemBuyout" or "bid"), nil, "OPT_83_NO_COPPER"))
				:Draw()
			detailsHeader1:GetElement("buyout.text")
				:SetText(Money.ToString(currentRow:GetField(ItemInfo.IsCommodity(itemString) and "itemBuyout" or "buyout"), nil, "OPT_83_NO_COPPER"))
				:Draw()
			detailsHeader2:GetElement("quantity.text")
				:SetText(TSM.IsWowClassic() and format(L["%d of %d"], rowStacksRemaining, currentRow:GetField("stackSize")) or currentRow:GetField("stackSize"))
				:Draw()
			local duration = nil
			if context.scanType == "POST" then
				duration = TSM.CONST.AUCTION_DURATIONS[currentRow:GetField("postTime")]
			elseif context.scanType == "CANCEL" then
				if TSM.IsWowClassic() then
					duration = _G["AUCTION_TIME_LEFT"..currentRow:GetField("duration")]
				else
					duration = currentRow:GetField("duration") - time()
					if duration < SECONDS_PER_MIN then
						duration = duration.."s"
					elseif duration < SECONDS_PER_HOUR then
						duration = floor(duration / SECONDS_PER_MIN).."m"
					elseif duration < SECONDS_PER_DAY then
						duration = floor(duration / SECONDS_PER_HOUR).."h"
					else
						duration = floor(duration / SECONDS_PER_DAY).."d"
					end
				end
			else
				error("Invalid scanType: "..tostring(context.scanType))
			end
			detailsHeader2:GetElement("duration.text")
				:SetText(duration)
				:Draw()
			if context.scanType == "POST" and context.itemString ~= itemString then
				fsmPrivate.UpdateDepositCost(context)
				context.itemString = itemString
			end
			header:GetElement("content.details.header.editBtn")
				:SetDisabled(context.scanType ~= "POST")
				:Draw()
		else
			itemContent:GetElement("icon")
				:SetBackground(nil)
				:SetTooltip(nil)
				:Draw()
			itemContent:GetElement("text")
				:SetText("-")
				:SetTooltip(nil)
				:Draw()
			header:GetElement("content.item.cost.text")
				:SetText("-")
				:Draw()
			header:GetElement("content.item.operation.text")
				:SetText("-")
				:Draw()
			detailsHeader1:GetElement("bid.text")
				:SetText("-")
				:Draw()
			detailsHeader1:GetElement("buyout.text")
				:SetText("-")
				:Draw()
			detailsHeader2:GetElement("quantity.text")
				:SetText("-")
				:Draw()
			detailsHeader2:GetElement("duration.text")
				:SetText("-")
				:Draw()
			if context.scanFrame:GetElement("tabs"):GetPath() == L["Auctioning Log"] then
				context.scanFrame:GetElement("tabs.logFrame.log")
					:SetSelection(nil)
					:Draw()
			end
			header:GetElement("content.details.header.editBtn")
				:SetDisabled(true)
				:Draw()
		end

		local processText = nil
		if context.scanType == "POST" then
			processText = L["Post"]
		elseif context.scanType == "CANCEL" then
			processText = CANCEL
		else
			error("Invalid scan type: "..tostring(context.scanType))
		end
		local bottom = context.scanFrame:GetElement("bottom")
		bottom:GetElement("processBtn")
			:SetText(processText)
		local progress, isPaused = context.auctionScan:GetProgress()
		local scanDone = progress == 1 and not context.isScanning
		local isPausePending = context.pausePending ~= nil
		if not isPausePending and (scanDone or isPaused) then
			-- we're done (or paused) scanning so start Posting/Canceling
			local doneStr, progressFmtStr = nil, nil
			if context.scanType == "POST" then
				doneStr = L["Done Posting"]
				progressFmtStr = (isPaused and (L["Scan Paused"].." | ") or "")..L["Posting %d / %d"]
			elseif context.scanType == "CANCEL" then
				doneStr = L["Done Canceling"]
				progressFmtStr = (isPaused and (L["Scan Paused"].." | ") or "")..L["Canceling %d / %d"]
			else
				error("Invalid scan type: "..tostring(context.scanType))
			end
			local progressText, iconHidden = nil, false
			if numConfirmed == totalNum then
				progressText = doneStr
				iconHidden = true
			elseif numProcessed == totalNum then
				progressText = format(L["Confirming %d / %d"], numConfirmed + 1, totalNum)
			elseif numProcessed == numConfirmed then
				progressText = format(progressFmtStr, numProcessed + 1, totalNum)
				iconHidden = true
			else
				progressText = format(progressFmtStr.." ("..L["Confirming %d / %d"]..")", numProcessed + 1, totalNum, numConfirmed + 1, totalNum)
			end
			bottom:GetElement("progressBar")
				:SetProgress(totalNum > 0 and (numProcessed / totalNum) or 1)
				:SetProgressIconHidden(iconHidden)
				:SetText(progressText)
			local deposit = context.scanType == "POST" and Money.FromString(header:GetElement("content.item.cost.text"):GetText())
			bottom:GetElement("processBtn"):SetDisabled(numProcessed == totalNum or (deposit and deposit > GetMoney()) or (not TSM.IsWowClassic() and context.pendingFuture))
			bottom:GetElement("skipBtn"):SetDisabled(numProcessed == totalNum)
			bottom:GetElement("pauseResumeBtn")
				:SetDisabled(numProcessed ~= numConfirmed or scanDone)
				:SetHighlightLocked(false)
		else
			-- we're scanning or pausing
			local text = nil
			if isPausePending then
				text = isPaused and L["Resuming Scan..."] or L["Pausing Scan..."]
			else
				local numItems = context.auctionScan:GetNumItems()
				text = numItems and format(L["Scanning (%d Items)"], numItems) or L["Scanning"]
			end
			bottom:GetElement("progressBar")
				:SetProgress(progress)
				:SetProgressIconHidden(false)
				:SetText(text)
			bottom:GetElement("processBtn"):SetDisabled(true)
			bottom:GetElement("skipBtn"):SetDisabled(true)
			bottom:GetElement("pauseResumeBtn")
				:SetDisabled(isPausePending or numProcessed == totalNum)
				:SetHighlightLocked(isPausePending)
		end
		bottom:Draw()
	end
	function fsmPrivate.ShowEditDialog(context)
		if context.scanType ~= "POST" then
			return
		end
		local currentRow = TSM.Auctioning.PostScan.GetCurrentRow()
		local itemString = currentRow:GetField("itemString")
		local isCommodity = ItemInfo.IsCommodity(itemString)
		local bid = currentRow:GetField(isCommodity and "itemBuyout" or "bid")
		local buyout = currentRow:GetField(isCommodity and "itemBuyout" or "buyout")

		private.perItem = true

		context.scanFrame:GetBaseElement():ShowDialogFrame(UIElements.New("Frame", "frame")
			:SetLayout("VERTICAL")
			:SetSize(328, 328)
			:SetPadding(12)
			:AddAnchor("CENTER")
			:SetBackgroundColor("FRAME_BG", true)
			:SetMouseEnabled(true)
			:AddChild(UIElements.New("Frame", "header")
				:SetLayout("HORIZONTAL")
				:SetHeight(24)
				:SetMargin(0, 0, -4, 10)
				:AddChild(UIElements.New("Spacer", "spacer")
					:SetWidth(24)
				)
				:AddChild(UIElements.New("Text", "title")
					:SetFont("BODY_BODY2_MEDIUM")
					:SetJustifyH("CENTER")
					:SetText(L["Edit Post"])
				)
				:AddChild(UIElements.New("Button", "closeBtn")
					:SetMargin(0, -4, 0, 0)
					:SetBackgroundAndSize("iconPack.24x24/Close/Default")
					:SetScript("OnClick", private.EditDialogCloseBtnOnClick)
				)
			)
			:AddChild(UIElements.New("Frame", "item")
				:SetLayout("HORIZONTAL")
				:SetPadding(6)
				:SetMargin(0, 0, 0, 16)
				:SetBackgroundColor("PRIMARY_BG_ALT", true)
				:AddChild(UIElements.New("Button", "icon")
					:SetSize(36, 36)
					:SetMargin(0, 8, 0, 0)
					:SetBackground(ItemInfo.GetTexture(itemString))
					:SetTooltip(itemString)
				)
				:AddChild(UIElements.New("Text", "name")
					:SetHeight(36)
					:SetFont("ITEM_BODY1")
					:SetText(TSM.UI.GetColoredItemName(itemString))
				)
			)
			-- TODO: implement editing stack sizes
			:AddChild(UIElements.New("Frame", "stacksFrame")
				:SetLayout("HORIZONTAL")
				:SetHeight(20)
				:SetMargin(0, 0, 0, 16)
				:AddChild(UIElements.New("Text", "stacksText")
					:SetWidth("AUTO")
					:SetMargin(0, 8, 0, 0)
					:SetFont("BODY_BODY2")
					:SetText(L["Stack(s)"]..":")
				)
				:AddChild(UIElements.New("Input", "stacksInput")
					:SetSize(62, 24)
					:SetMargin(0, 16, 0, 0)
					:SetBackgroundColor("PRIMARY_BG_ALT")
					:SetValidateFunc("NUMBER", "1:5000")
					:SetDisabled(true)
					:SetValue(currentRow:GetField("numStacks"))
				)
				:AddChild(UIElements.New("Text", "quantityText")
					:SetWidth("AUTO")
					:SetMargin(0, 8, 0, 0)
					:SetFont("BODY_BODY2")
					:SetText(L["Quantity"]..":")
				)
				:AddChild(UIElements.New("Input", "quantityInput")
					:SetSize(62, 24)
					:SetBackgroundColor("PRIMARY_BG_ALT")
					:SetValidateFunc("NUMBER", "1:5000")
					:SetDisabled(true)
					:SetValue(currentRow:GetField("stackSize"))
				)
			)
			:AddChild(UIElements.New("Frame", "duration")
				:SetLayout("HORIZONTAL")
				:SetHeight(24)
				:SetMargin(0, 0, 0, 24)
				:AddChild(UIElements.New("Text", "desc")
					:SetWidth("AUTO")
					:SetFont("BODY_BODY2")
					:SetText(L["Duration"]..":")
				)
				:AddChild(UIElements.New("Toggle", "toggle")
					:SetMargin(0, 48, 0, 0)
					:AddOption(TSM.CONST.AUCTION_DURATIONS[1])
					:AddOption(TSM.CONST.AUCTION_DURATIONS[2])
					:AddOption(TSM.CONST.AUCTION_DURATIONS[3])
					:SetOption(TSM.CONST.AUCTION_DURATIONS[currentRow:GetField("postTime")])
				)
			)
			:AddChild(UIElements.New("Frame", "per")
				:SetLayout("HORIZONTAL")
				:SetHeight(20)
				:SetMargin(0, 0, 0, 8)
				:AddChild(UIElements.New("Spacer", "spacer"))
				:AddChild(UIElements.New("Button", "item")
					:SetWidth("AUTO")
					:SetMargin(0, 8, 0, 0)
					:SetFont("BODY_BODY2_MEDIUM")
					:SetJustifyH("RIGHT")
					:SetTextColor("INDICATOR")
					:SetText(L["Per Item"])
					:SetScript("OnClick", TSM.IsWowClassic() and private.PerItemOnClick)
				)
				:AddChildIf(TSM.IsWowClassic(), UIElements.New("Button", "stack")
					:SetWidth("AUTO")
					:SetTextColor("TEXT")
					:SetFont("BODY_BODY2_MEDIUM")
					:SetJustifyH("RIGHT")
					:SetTextColor("INDICATOR")
					:SetText(L["Per Stack"])
					:SetScript("OnClick", TSM.IsWowClassic() and private.PerStackOnClick)
				)
			)
			:AddChild(UIElements.New("Frame", "bid")
				:SetLayout("HORIZONTAL")
				:SetHeight(24)
				:SetMargin(0, 0, 0, 10)
				:AddChild(UIElements.New("Text", "desc")
					:SetWidth("AUTO")
					:SetFont("BODY_BODY2")
					:SetText(L["Bid Price"]..":")
				)
				:AddChild(UIElements.New("Spacer", "spacer"))
				:AddChild(UIElements.New("Input", "input")
					:SetWidth(132)
					:SetBackgroundColor("PRIMARY_BG_ALT")
					:SetFont("TABLE_TABLE1")
					:SetValidateFunc(private.BidBuyoutValidateFunc)
					:SetJustifyH("RIGHT")
					:SetDisabled(isCommodity)
					:SetTabPaths("__parent.__parent.buyout.input", "__parent.__parent.buyout.input")
					:SetContext("bid")
					:SetValue(Money.ToString(bid, nil, "OPT_83_NO_COPPER"))
					:SetScript("OnValidationChanged", private.BidBuyoutOnValidationChanged)
					:SetScript("OnValueChanged", private.BidBuyoutInputOnValueChanged)
					:SetScript("OnEnterPressed", private.BidBuyoutInputOnEnterPressed)
				)
			)
			:AddChild(UIElements.New("Frame", "buyout")
				:SetLayout("HORIZONTAL")
				:SetHeight(24)
				:SetMargin(0, 0, 0, 16)
				:AddChild(UIElements.New("Text", "desc")
					:SetWidth("AUTO")
					:SetFont("BODY_BODY2")
					:SetText(L["Buyout Price"]..":")
				)
				:AddChild(UIElements.New("Spacer", "spacer"))
				:AddChild(UIElements.New("Input", "input")
					:SetWidth(132)
					:SetBackgroundColor("PRIMARY_BG_ALT")
					:SetFont("TABLE_TABLE1")
					:SetValidateFunc(private.BidBuyoutValidateFunc)
					:SetJustifyH("RIGHT")
					:SetContext(isCommodity and "itemBuyout" or "buyout")
					:SetTabPaths("__parent.__parent.bid.input", "__parent.__parent.bid.input")
					:SetValue(Money.ToString(buyout, nil, "OPT_83_NO_COPPER"))
					:SetScript("OnValidationChanged", private.BidBuyoutOnValidationChanged)
					:SetScript("OnValueChanged", private.BidBuyoutInputOnValueChanged)
					:SetScript("OnEnterPressed", private.BidBuyoutInputOnEnterPressed)
				)
			)
			:AddChild(UIElements.New("ActionButton", "saveBtn")
				:SetHeight(24)
				:SetText(SAVE)
				:SetContext(context)
				:SetScript("OnClick", fsmPrivate.EditPostingDetailsSaveOnClick)
			)
		)
	end
	function fsmPrivate.EditPostingDetailsSaveOnClick(button)
		local context = button:GetContext()
		assert(context.scanType == "POST")
		local currentRow = TSM.Auctioning.PostScan.GetCurrentRow()

		local buyout = private.ParseBidBuyout(button:GetElement("__parent.buyout.input"):GetValue())
		local bid = min(private.ParseBidBuyout(button:GetElement("__parent.bid.input"):GetValue()), buyout)
		if not TSM.IsWowClassic() and ItemInfo.IsCommodity(context.itemString) and bid ~= buyout then
			Log.PrintUser(L["Did not change prices due to an invalid bid or buyout value."])
		else
			local duration = Table.KeyByValue(TSM.CONST.AUCTION_DURATIONS, button:GetElement("__parent.duration.toggle"):GetValue())
			if duration ~= currentRow:GetField("postTime") then
				TSM.Auctioning.PostScan.ChangePostDetail("postTime", duration)
			end

			-- update buyout first since doing so may change the bid
			if buyout ~= currentRow:GetField("buyout") then
				TSM.Auctioning.PostScan.ChangePostDetail("buyout", private.perItem and buyout or buyout / currentRow:GetField("stackSize"))
			end

			if not ItemInfo.IsCommodity(context.itemString) and bid ~= currentRow:GetField("bid") then
				TSM.Auctioning.PostScan.ChangePostDetail("bid", private.perItem and bid or bid / currentRow:GetField("stackSize"))
			end
		end

		fsmPrivate.UpdateDepositCost(context)
		fsmPrivate.UpdateScanFrame(context)
		button:GetBaseElement():HideDialog()
	end
	private.fsm = FSM.New("AUCTIONING")
		:AddState(FSM.NewState("ST_INIT")
			:SetOnEnter(function(context, scanType, scanContext)
				private.hasLastScan = false
				TSM.Auctioning.Log.Truncate()
				TSM.Auctioning.PostScan.Reset()
				TSM.Auctioning.CancelScan.Reset()

				if context.scanThreadId then
					Threading.Kill(context.scanThreadId)
					context.scanThreadId = nil
				end
				context.pausePending = nil
				context.itemString = nil
				context.isScanning = false
				if context.auctionScan then
					context.auctionScan:Release()
					context.auctionScan = nil
					private.auctionScan = context.auctionScan
				end
				if context.pendingFuture then
					context.pendingFuture:Cancel()
					context.pendingFuture = nil
				end

				if scanType then
					return "ST_STARTING_SCAN", scanType, scanContext
				elseif context.scanFrame then
					context.scanFrame:GetParentElement():SetPath("selection", true)
					context.scanFrame = nil
				end
				TSM.UI.AuctionUI.EndedScan(L["Auctioning"])
			end)
			:AddTransition("ST_INIT")
			:AddTransition("ST_STARTING_SCAN")
		)
		:AddState(FSM.NewState("ST_STARTING_SCAN")
			:SetOnEnter(function(context, scanType, scanContext)
				private.hasLastScan = true
				context.scanType = scanType
				if context.scanType == "POST" then
					context.scanThreadId = TSM.Auctioning.PostScan.Prepare()
					private.logQuery:ResetOrderBy()
					private.logQuery:OrderBy("index", true)
				elseif context.scanType == "CANCEL" then
					context.scanThreadId = TSM.Auctioning.CancelScan.Prepare()
					private.logQuery:ResetOrderBy()
					private.logQuery:OrderBy("index", false)
				else
					error("Invalid scan type: "..tostring(context.scanType))
				end
				context.auctionScan = AuctionScan.GetManager()
					:SetResolveSellers(true)
					:SetScript("OnProgressUpdate", private.FSMAuctionScanOnProgressUpdate)
				private.auctionScan = context.auctionScan
				fsmPrivate.UpdateScanFrame(context)
				Threading.SetCallback(context.scanThreadId, private.FSMScanCallback)
				Threading.Start(context.scanThreadId, context.auctionScan, scanContext)
				return "ST_SCANNING"
			end)
			:AddTransition("ST_SCANNING")
		)
		:AddState(FSM.NewState("ST_SCANNING")
			:SetOnEnter(function(context)
				context.isScanning = true
				fsmPrivate.UpdateScanFrame(context)
			end)
			:SetOnExit(function(context)
				context.isScanning = false
			end)
			:AddTransition("ST_RESULTS")
			:AddTransition("ST_INIT")
			:AddEvent("EV_SCAN_PROGRESS_UPDATE", function(context)
				local _, isPaused = context.auctionScan:GetProgress()
				if context.pausePending == isPaused then
					context.pausePending = nil
				end
				if isPaused and context.pausePending == nil then
					return "ST_RESULTS"
				else
					fsmPrivate.UpdateScanFrame(context)
				end
			end)
			:AddEvent("EV_SCAN_COMPLETE", function(context)
				Sound.PlaySound(private.settings.scanCompleteSound)
				return "ST_RESULTS"
			end)
			:AddEvent("EV_PAUSE_RESUME_CLICKED", function(context)
				assert(context.pausePending == nil)
				context.pausePending = true
				context.auctionScan:SetPaused(true)
				fsmPrivate.UpdateScanFrame(context)
			end)
			:AddEvent("EV_EDIT_BUTTON_CLICKED", function(context)
				fsmPrivate.ShowEditDialog(context)
			end)
		)
		:AddState(FSM.NewState("ST_HANDLING_CONFIRM")
			:SetOnEnter(function(context, success, canRetry)
				local isDone = false
				if context.scanType == "POST" then
					TSM.Auctioning.PostScan.HandleConfirm(success, canRetry)
					local _, numConfirmed, numFailed, totalNum = TSM.Auctioning.PostScan.GetStatus()
					if numConfirmed == totalNum then
						if numFailed > 0 then
							-- TODO: need to wait for the player's bags to settle
							Log.PrintfUser(L["Retrying %d auction(s) which failed."], numFailed)
							TSM.Auctioning.PostScan.PrepareFailedPosts()
						else
							isDone = true
						end
					end
				elseif context.scanType == "CANCEL" then
					TSM.Auctioning.CancelScan.HandleConfirm(success, canRetry)
					local _, numConfirmed, numFailed, totalNum = TSM.Auctioning.CancelScan.GetStatus()
					if numConfirmed == totalNum then
						if numFailed > 0 then
							-- TODO: need to wait for the player's auctions to settle
							Log.PrintfUser(L["Retrying %d auction(s) which failed."], numFailed)
							TSM.Auctioning.CancelScan.PrepareFailedCancels()
						else
							isDone = true
						end
					end
				else
					error("Invalid scan type: "..tostring(context.scanType))
				end
				local _, isPaused = context.auctionScan:GetProgress()
				if not isDone then
					return "ST_RESULTS"
				elseif isPaused then
					-- unpause the scan now that we're done
					assert(context.pausePending == nil)
					context.pausePending = false
					context.auctionScan:SetPaused(false)
					return "ST_SCANNING"
				else
					return "ST_DONE"
				end
			end)
			:AddTransition("ST_SCANNING")
			:AddTransition("ST_RESULTS")
			:AddTransition("ST_DONE")
		)
		:AddState(FSM.NewState("ST_RESULTS")
			:SetOnEnter(function(context)
				local _, isPaused = context.auctionScan:GetProgress()
				if not isPaused then
					TSM.UI.AuctionUI.EndedScan(L["Auctioning"])
					Threading.Kill(context.scanThreadId)
				end
				fsmPrivate.UpdateScanFrame(context)
			end)
			:AddTransition("ST_INIT")
			:AddTransition("ST_HANDLING_CONFIRM")
			:AddTransition("ST_SCANNING")
			:AddTransition("ST_DONE")
			:AddEvent("EV_PROCESS_CLICKED", function(context)
				context.scanFrame:GetBaseElement():HideDialog()
				local result, noRetry = nil, nil
				if context.scanType == "POST" then
					result, noRetry = TSM.Auctioning.PostScan.DoProcess()
				elseif context.scanType == "CANCEL" then
					result, noRetry = TSM.Auctioning.CancelScan.DoProcess()
				else
					error("Invalid scan type: "..tostring(context.scanType))
				end
				if not result then
					-- we failed to post / cancel
					return "ST_HANDLING_CONFIRM", false, not noRetry
				elseif not TSM.IsWowClassic() then
					context.pendingFuture = result
					context.pendingFuture:SetScript("OnDone", private.FSMPendingFutureOneDone)
				end
				fsmPrivate.UpdateScanFrame(context)
			end)
			:AddEvent("EV_SKIP_CLICKED", function(context)
				local isDone = nil
				if context.scanType == "POST" then
					TSM.Auctioning.PostScan.DoSkip()
					local _, numConfirmed, numFailed, totalNum = TSM.Auctioning.PostScan.GetStatus()
					isDone = numConfirmed == totalNum and numFailed == 0
				elseif context.scanType == "CANCEL" then
					TSM.Auctioning.CancelScan.DoSkip()
					local _, numConfirmed, numFailed, totalNum = TSM.Auctioning.CancelScan.GetStatus()
					isDone = numConfirmed == totalNum and numFailed == 0
				else
					error("Invalid scan type: "..tostring(context.scanType))
				end
				fsmPrivate.UpdateScanFrame(context)
				local _, isPaused = context.auctionScan:GetProgress()
				if isDone and isPaused then
					-- unpause the scan now that we're done
					assert(context.pausePending == nil)
					context.pausePending = false
					context.auctionScan:SetPaused(false)
					return "ST_SCANNING"
				elseif isDone then
					return "ST_DONE"
				end
			end)
			:AddEvent("EV_PENDING_FUTURE_DONE", function(context)
				assert(context.pendingFuture:IsDone())
				local value = context.pendingFuture:GetValue()
				context.pendingFuture = nil
				if value == true then
					return "ST_HANDLING_CONFIRM", true, false
				elseif value == false then
					return "ST_HANDLING_CONFIRM", false, true
				elseif value == nil then
					return "ST_HANDLING_CONFIRM", false, false
				else
					error("Invalid value: "..tostring(value))
				end
			end)
			:AddEvent("EV_AUCTION_POST_CONFIRM", function(context, success, canRetry)
				if context.scanType == "POST" then
					return "ST_HANDLING_CONFIRM", success, canRetry
				end
			end)
			:AddEvent("EV_AUCTION_CANCEL_CONFIRM", function(context, success, canRetry)
				if context.scanType == "CANCEL" then
					return "ST_HANDLING_CONFIRM", success, canRetry
				end
			end)
			:AddEvent("EV_PAUSE_RESUME_CLICKED", function(context)
				assert(context.pausePending == nil)
				context.pausePending = false
				context.auctionScan:SetPaused(false)
				return "ST_SCANNING"
			end)
			:AddEvent("EV_EDIT_BUTTON_CLICKED", function(context)
				fsmPrivate.ShowEditDialog(context)
			end)
		)
		:AddState(FSM.NewState("ST_DONE")
			:SetOnEnter(function(context)
				private.canStartNewScan = true
				AuctionTracking.QueryOwnedAuctions()
				Sound.PlaySound(private.settings.confirmCompleteSound)
				fsmPrivate.UpdateScanFrame(context)
			end)
			:SetOnExit(function(context)
				private.canStartNewScan = false
			end)
			:AddTransition("ST_INIT")
		)
		:AddDefaultEvent("EV_START_SCAN", function(context, scanType, scanContext)
			return "ST_INIT", scanType, scanContext
		end)
		:AddDefaultEvent("EV_SCAN_FRAME_SHOWN", function(context, scanFrame)
			context.scanFrame = scanFrame
			fsmPrivate.UpdateScanFrame(context)
		end)
		:AddDefaultEvent("EV_SCAN_FRAME_HIDDEN", function(context)
			context.scanFrame = nil
			context.itemString = nil
		end)
		:AddDefaultEventTransition("EV_BACK_BUTTON_CLICKED", "ST_INIT")
		:AddDefaultEventTransition("EV_AUCTION_HOUSE_CLOSED", "ST_INIT")
		:AddDefaultEvent("EV_PENDING_FUTURE_DONE", function(context)
			error("Unexpected pending future done event")
		end)
		:Init("ST_INIT", fsmContext)
end

function private.FSMAuctionScanOnProgressUpdate(auctionScan)
	-- this even is very spammy while we scan, so silence the FSM logging
	private.fsm:SetLoggingEnabled(false)
	private.fsm:ProcessEvent("EV_SCAN_PROGRESS_UPDATE")
	private.fsm:SetLoggingEnabled(true)
end

function private.FSMScanCallback()
	private.fsm:ProcessEvent("EV_SCAN_COMPLETE")
end

function private.FSMPendingFutureOneDone(value)
	private.fsm:ProcessEvent("EV_PENDING_FUTURE_DONE")
end



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

function private.BagScrollingTableIsSelectionEnabled(_, record)
	return record:GetField("firstOperation") and true or false
end

function private.BagGetOperationText(firstOperation)
	return firstOperation or Theme.GetFeedbackColor("RED"):ColorText(L["Skipped: No assigned operation"])
end

function private.LogGetBuyoutText(buyout)
	return buyout == 0 and "-" or Money.ToString(buyout, nil, "OPT_83_NO_COPPER")
end

function private.LogGetIndexIcon(row)
	if row:GetField("state") == "PENDING" then
		-- color the circle icon to match the color of the text
		return TSM.UI.TexturePacks.GetColoredKey("iconPack.12x12/Circle", TSM.Auctioning.Log.GetColorFromReasonKey(row:GetField("reasonKey")))
	else
		return "iconPack.12x12/Checkmark/Default"
	end
end

function private.MarketValueFunction(row)
	return CustomPrice.GetValue("dbmarket", row:GetItemString() or row:GetBaseItemString())
end

function private.EditDialogCloseBtnOnClick(button)
	button:GetBaseElement():HideDialog()
end

function private.PerItemOnClick(button)
	if private.perItem then
		return
	end

	private.perItem = true
	button:GetElement("__parent.stack")
		:SetTextColor("TEXT")
		:Draw()
	button:SetTextColor("INDICATOR")
		:Draw()

	local row = TSM.Auctioning.PostScan.GetCurrentRow()
	local bidInput = button:GetElement("__parent.__parent.bid.input")
	local buyoutInput = button:GetElement("__parent.__parent.buyout.input")
	buyoutInput:SetFocused(false)
		:SetValue(row:GetField("buyout"))
		:Draw()
	bidInput:SetFocused(false)
		:SetValue(row:GetField("bid"))
		:Draw()
end

function private.PerStackOnClick(button)
	if not private.perItem then
		return
	end

	private.perItem = false
	button:GetElement("__parent.item")
		:SetTextColor("TEXT")
		:Draw()
	button:SetTextColor("INDICATOR")
		:Draw()

	local row = TSM.Auctioning.PostScan.GetCurrentRow()
	local stackSize = row:GetField("stackSize")
	local bidInput = button:GetElement("__parent.__parent.bid.input")
	local buyoutInput = button:GetElement("__parent.__parent.buyout.input")
	buyoutInput:SetFocused(false)
		:SetValue(row:GetField("buyout") * stackSize)
		:Draw()
	bidInput:SetFocused(false)
		:SetValue(row:GetField("bid") * stackSize)
		:Draw()
end

function private.UpdateSaveButtonState(frame)
	local bidInput = frame:GetElement("bid.input")
	local buyoutInput = frame:GetElement("buyout.input")
	local bid = private.ParseBidBuyout(bidInput:GetValue())
	local buyout = private.ParseBidBuyout(buyoutInput:GetValue())
	frame:GetElement("saveBtn")
		:SetDisabled(bid > buyout or not bidInput:IsValid() or not buyoutInput:IsValid())
		:Draw()
end

function private.BidBuyoutOnValidationChanged(input)
	private.UpdateSaveButtonState(input:GetElement("__parent.__parent"))
end

function private.BidBuyoutInputOnValueChanged(input)
	local frame = input:GetElement("__parent.__parent")
	local context = frame:GetElement("saveBtn"):GetContext()
	local bidInput = frame:GetElement("bid.input")
	local buyoutInput = frame:GetElement("buyout.input")
	local bid = private.ParseBidBuyout(bidInput:GetValue())
	local buyout = private.ParseBidBuyout(buyoutInput:GetValue())
	if input == buyoutInput and not TSM.IsWowClassic() and ItemInfo.IsCommodity(context.itemString) then
		-- update the bid to match
		bidInput:SetValue(Money.ToString(buyout, nil, "OPT_83_NO_COPPER", "OPT_DISABLE"))
			:Draw()
	elseif input == bidInput and private.ParseBidBuyout(input:GetValue()) > private.ParseBidBuyout(buyoutInput:GetValue()) then
		-- update the buyout to match
		buyoutInput:SetValue(Money.ToString(bid, nil, "OPT_83_NO_COPPER"))
			:Draw()
	end
	private.UpdateSaveButtonState(frame)
end

function private.BidBuyoutInputOnEnterPressed(input)
	local frame = input:GetElement("__parent.__parent")
	local bidInput = frame:GetElement("bid.input")
	local buyoutInput = frame:GetElement("buyout.input")
	local value = private.ParseBidBuyout(input:GetValue())
	input:SetValue(Money.ToString(value, nil, "OPT_83_NO_COPPER"))
	input:Draw()
	if input == buyoutInput and private.ParseBidBuyout(buyoutInput:GetValue()) < private.ParseBidBuyout(bidInput:GetValue()) then
		-- update the bid to match
		bidInput:SetValue(Money.ToString(value, nil, "OPT_83_NO_COPPER"))
			:Draw()
	end
	private.UpdateSaveButtonState(frame)
end

function private.ParseBidBuyout(value)
	value = Money.FromString(value) or tonumber(value)
	if not value then
		return nil
	end
	if not TSM.IsWowClassic() and value % COPPER_PER_SILVER ~= 0 then
		return nil
	end
	return (value or 0) > 0 and value <= MAXIMUM_BID_PRICE and value or nil
end

function private.BidBuyoutValidateFunc(input, value)
	value = private.ParseBidBuyout(value)
	if not value then
		return false, L["Invalid price."]
	end
	return true
end
