-- ------------------------------------------------------------------------------ --
--                                TradeSkillMaster                                --
--                http://www.curse.com/addons/wow/tradeskill-master               --
--                                                                                --
--             A TradeSkillMaster Addon (http://tradeskillmaster.com)             --
--    All Rights Reserved* - Detailed license information included with addon.    --
-- ------------------------------------------------------------------------------ --

local _, TSM = ...
local Crafting = TSM.UI.CraftingUI:NewPackage("Crafting")
local L = LibStub("AceLocale-3.0"):GetLocale("TradeSkillMaster") -- loads the localization table
local private = {
	db = nil,
	fsm = nil,
	professionsOrder = {},
	professions = {},
	page = "profession",
	groupSearch = "",
	showDelayFrame = 0,
	professionQuery = nil,
}
local SHOW_DELAY_FRAMES = 2
local KEY_SEP = "\001"
local CRAFTING_PAGES = {
	profession = L["Professions"],
	group = L["Groups"]
}
local CRAFTING_PAGES_ORDER = { "profession", "group" }
local DEFAULT_DIVIDED_CONTAINER_CONTEXT = {
	leftWidth = 496,
}
local MINING_SPELLID = 2575
local SMELTING_SPELLID = 2656
-- TODO: this should eventually go in the saved variables
private.dividedContainerContext = {}



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

function Crafting.OnInitialize()
	TSM.UI.CraftingUI.RegisterTopLevelPage("Crafting", "iconPack.24x24/Crafting", private.GetCraftingFrame)
	private.FSMCreate()
end

function Crafting.GatherCraftNext(spellId, quantity)
	private.fsm:ProcessEvent("EV_CRAFT_NEXT_BUTTON_CLICKED", spellId, quantity)
end

-- ============================================================================
-- Crafting UI
-- ============================================================================

function private.GetCraftingFrame()
	local frame = TSMAPI_FOUR.UI.NewElement("DividedContainer", "crafting")
		:SetContextTable(private.dividedContainerContext, DEFAULT_DIVIDED_CONTAINER_CONTEXT)
		:SetMinWidth(450, 250)
		:SetLeftChild(TSMAPI_FOUR.UI.NewElement("Frame", "left")
			:SetLayout("VERTICAL")
			:SetStyle("background", "#272727")
			:AddChild(TSMAPI_FOUR.UI.NewElement("Frame", "header")
				:SetLayout("HORIZONTAL")
				:SetStyle("height", 25)
				:SetStyle("margin", { top = 4, left = 8, right = 8, bottom = 4 })
				:AddChild(TSMAPI_FOUR.UI.NewElement("Dropdown", "dropdown")
					:SetStyle("width", 125)
					:SetStyle("textPadding", 4)
					:SetStyle("background", "#00000000")
					:SetStyle("border", "#00000000")
					:SetStyle("font", TSM.UI.Fonts.title)
					:SetStyle("fontHeight", 16)
					:SetStyle("openFont", TSM.UI.Fonts.title)
					:SetStyle("openFontHeight", 16)
					:SetDictionaryItems(CRAFTING_PAGES, CRAFTING_PAGES[private.page], CRAFTING_PAGES_ORDER)
					:SetSettingInfo(private, "page")
					:SetScript("OnSelectionChanged", private.DropdownOnSelectionChanged)
				)
				:AddChild(TSMAPI_FOUR.UI.NewElement("Spacer", "spacer"))
			)
			:AddChild(TSMAPI_FOUR.UI.NewElement("ViewContainer", "content")
				:SetNavCallback(private.GetCraftingElements)
				:AddPath("profession", private.page == "profession")
				:AddPath("group", private.page == "group")
			)
		)
		:SetRightChild(TSMAPI_FOUR.UI.NewElement("Frame", "right")
			:SetLayout("VERTICAL")
			:SetStyle("background", "#171717")
			:SetStyle("padding", { left = 4, right = 4, bottom = 4 })
			:AddChild(TSMAPI_FOUR.UI.NewElement("Frame", "details")
				:SetLayout("VERTICAL")
				:SetStyle("height", 191)
				:SetStyle("background", "#404040")
				:SetStyle("padding", { top = 35, bottom = 4, left = 8, right = 8 })
				:SetStyle("margin", { bottom = 4 })
				:AddChild(TSMAPI_FOUR.UI.NewElement("Frame", "title")
					:SetLayout("HORIZONTAL")
					:SetStyle("height", 22)
					:AddChild(TSMAPI_FOUR.UI.NewElement("Text", "name")
						:SetStyle("font", TSM.UI.Fonts.title)
						:SetStyle("fontHeight", 16)
					)
					:AddChild(TSMAPI_FOUR.UI.NewElement("Frame", "rank")
						:SetLayout("HORIZONTAL")
						:SetStyle("width", 45)
						:SetStyle("height", 14)
						:SetStyle("padding", { left = 4.5, right = 4.5 })
						:SetStyle("backgroundTexturePack", "uiFrames.RankFrame")
						:AddChild(TSMAPI_FOUR.UI.NewElement("Texture", "star1")
							:SetStyle("width", 10)
							:SetStyle("height", 10)
						)
						:AddChild(TSMAPI_FOUR.UI.NewElement("Texture", "star2")
							:SetStyle("width", 10)
							:SetStyle("height", 10)
							:SetStyle("margin", { left = 3, right = 3 })
						)
						:AddChild(TSMAPI_FOUR.UI.NewElement("Texture", "star3")
							:SetStyle("width", 10)
							:SetStyle("height", 10)
						)
					)
					:AddChild(TSMAPI_FOUR.UI.NewElement("Button", "helpBtn")
						:SetStyle("width", 14)
						:SetStyle("height", 14)
						:SetStyle("backgroundTexturePack", "iconPack.14x14/Help")
					)
				)
				:AddChild(TSMAPI_FOUR.UI.NewElement("Frame", "reagentsTitle")
					:SetLayout("HORIZONTAL")
					:SetStyle("height", 16)
					:AddChild(TSMAPI_FOUR.UI.NewElement("Text", "label")
						:SetStyle("margin", { right = 4 })
						:SetStyle("autoWidth", true)
						:SetStyle("font", TSM.UI.Fonts.bold)
						:SetStyle("fontHeight", 12)
						:SetText(L["Material List"])
					)
					:AddChild(TSMAPI_FOUR.UI.NewElement("Text", "error")
						:SetStyle("font", TSM.UI.Fonts.bold)
						:SetStyle("fontHeight", 12)
						:SetStyle("textColor", "#ff0000")
					)
				)
				:AddChild(TSMAPI_FOUR.UI.NewElement("CraftingMatList", "matList")
					:SetStyle("height", 64)
					:SetStyle("margin", { top = 2, left = -4, right = -4, bottom = 4 })
				)
				:AddChild(TSMAPI_FOUR.UI.NewElement("Frame", "queue")
					:SetLayout("HORIZONTAL")
					:SetStyle("height", 20)
					:AddChild(TSMAPI_FOUR.UI.NewElement("InputNumeric", "queueInput")
						:SetStyle("width", 96)
						:SetStyle("backgroundTexturePacks", "uiFrames.ActiveInputField")
						:SetStyle("margin", { right = 4 })
						:SetStyle("justifyH", "CENTER")
						:SetStyle("font", TSM.UI.Fonts.bold)
						:SetStyle("fontHeight", 16)
						:SetText(1)
						:SetScript("OnEnterPressed", private.QueueInputOnEnterPressed)
					)
					:AddChild(TSMAPI_FOUR.UI.NewElement("Button", "minusBtn")
						:SetStyle("width", 18)
						:SetStyle("height", 18)
						:SetStyle("backgroundTexturePack", "iconPack.18x18/Subtract/Circle")
						:SetScript("OnClick", private.MinusBtnOnClick)
					)
					:AddChild(TSMAPI_FOUR.UI.NewElement("Button", "plusBtn")
						:SetStyle("width", 18)
						:SetStyle("height", 18)
						:SetStyle("backgroundTexturePack", "iconPack.18x18/Add/Circle")
						:SetScript("OnClick", private.PlusBtnOnClick)
					)
					:AddChild(TSMAPI_FOUR.UI.NewElement("ActionButton", "queueBtn")
						:SetStyle("margin", { left = 12 })
						:SetText(L["Add to Queue"])
						:SetScript("OnClick", private.QueueBtnOnClick)
					)
				)
				:AddChild(TSMAPI_FOUR.UI.NewElement("Frame", "craft")
					:SetLayout("HORIZONTAL")
					:SetStyle("height", 20)
					:SetStyle("margin", { top = 4 })
					:AddChild(TSMAPI_FOUR.UI.NewElement("ActionButton", "craftBtn")
						:SetStyle("width", 100)
						:SetStyle("margin", { right = 8 })
						:SetText(L["Craft"])
						:SetScript("OnClick", private.CraftBtnOnClick)
					)
					:AddChild(TSMAPI_FOUR.UI.NewElement("ActionButton", "craftAllBtn")
						:SetText(L["Craft All"])
						:SetScript("OnClick", private.CraftAllBtnOnClick)
					)
				)
			)
			:AddChild(TSMAPI_FOUR.UI.NewElement("Frame", "queue")
				:SetLayout("VERTICAL")
				:SetStyle("background", "#404040")
				:SetStyle("padding", { top = 6, bottom = 4, left = 8, right = 8 })
				:AddChild(TSMAPI_FOUR.UI.NewElement("Text", "label")
					:SetStyle("height", 16)
					:SetStyle("font", TSM.UI.Fonts.bold)
					:SetStyle("fontHeight", 12)
					:SetText(L["Crafting Queue"])
				)
				:AddChild(TSMAPI_FOUR.UI.NewElement("CraftingQueueList", "queueList")
					:SetStyle("margin", { top = 2, left = -4, right = -4, bottom = 6 })
					:SetStyle("background", "#1c1c1c")
					:SetDatabaseQuery(TSM.Crafting.Queue.CreateQuery())
					:SetScript("OnRowClick", private.QueueOnRowClick)
				)
				:AddChild(TSMAPI_FOUR.UI.NewElement("Frame", "queueCost")
					:SetLayout("HORIZONTAL")
					:SetStyle("height", 15)
					:AddChild(TSMAPI_FOUR.UI.NewElement("Text", "label")
						:SetStyle("autoWidth", true)
						:SetStyle("fontHeight", 12)
						:SetText(L["Estimated Cost:"])
					)
					:AddChild(TSMAPI_FOUR.UI.NewElement("Text", "text")
						:SetStyle("font", TSM.UI.Fonts.number)
						:SetStyle("fontHeight", 12)
						:SetStyle("justifyH", "RIGHT")
					)
				)
				:AddChild(TSMAPI_FOUR.UI.NewElement("Frame", "queueProfit")
					:SetLayout("HORIZONTAL")
					:SetStyle("height", 15)
					:AddChild(TSMAPI_FOUR.UI.NewElement("Text", "label")
						:SetStyle("autoWidth", true)
						:SetStyle("fontHeight", 12)
						:SetText(L["Estimated Profit:"])
					)
					:AddChild(TSMAPI_FOUR.UI.NewElement("Text", "text")
						:SetStyle("font", TSM.UI.Fonts.number)
						:SetStyle("fontHeight", 12)
						:SetStyle("justifyH", "RIGHT")
					)
				)
				:AddChild(TSMAPI_FOUR.UI.NewElement("Frame", "craft")
					:SetLayout("HORIZONTAL")
					:SetStyle("height", 20)
					:SetStyle("margin", { top = 4 })
					:AddChild(TSMAPI_FOUR.UI.NewElement("ActionButton", "craftNextBtn", "TSMCraftingBtn")
						:SetStyle("width", 180)
						:SetStyle("margin", { right = 8 })
						:SetStyle("font", TSM.UI.Fonts.title)
						:SetStyle("fontHeight", 14)
						:SetText(L["Craft Next in Queue"])
						:SetScript("OnClick", private.CraftNextOnClick)
					)
					:AddChild(TSMAPI_FOUR.UI.NewElement("Button", "clearBtn")
						:SetStyle("font", TSM.UI.Fonts.title)
						:SetStyle("fontHeight", 12)
						:SetText(L["Clear Queue"])
						:SetScript("OnClick", TSM.Crafting.Queue.Clear)
					)
				)
			)
		)
		:SetScript("OnUpdate", private.FrameOnUpdate)
		:SetScript("OnHide", private.FrameOnHide)

	frame:GetElement("right.details"):Hide()
	frame:GetElement("right.queue"):_GetStyle("padding").top = 35
	return frame
end

function private.GetCraftingElements(self, button)
	if button == "profession" then
		if not private.professionQuery then
			private.professionQuery = TSM.Crafting.ProfessionScanner.CreateQuery()
		end
		private.professionQuery:Reset()
		local frame = TSMAPI_FOUR.UI.NewElement("Frame", "profession")
			:SetLayout("VERTICAL")
			:AddChild(TSMAPI_FOUR.UI.NewElement("Dropdown", "professionDropdown")
				:SetStyle("height", 26)
				:SetStyle("margin", { left = 8, right = 8, bottom = 8 })
				:SetHintText(L["No Profession Opened"])
				:SetScript("OnSelectionChanged", private.ProfessionDropdownOnSelectionChanged)
			)
			:AddChild(TSMAPI_FOUR.UI.NewElement("Frame", "searchFilter")
				:SetLayout("HORIZONTAL")
				:SetStyle("height", 20)
				:SetStyle("margin", { left = 8, right = 8, bottom = 11 })
				:AddChild(TSMAPI_FOUR.UI.NewElement("SearchInput", "filterInput")
					:SetStyle("margin", { right = 8 })
					:SetHintText(L["Search Patterns"])
					:SetScript("OnEnterPressed", private.FilterSearchInputOnEnterPressed)
				)
				:AddChild(TSMAPI_FOUR.UI.NewElement("Button", "filterBtn")
					:SetStyle("width", 36)
					:SetStyle("height", 14)
					:SetStyle("font", TSM.UI.Fonts.bold)
					:SetStyle("fontHeight", 11)
					:SetStyle("justifyH", "RIGHT")
					:SetText(FILTERS)
					-- TODO
					-- :SetScript("OnClick", private.FilterSearchButtonOnClick)
				)
				:AddChild(TSMAPI_FOUR.UI.NewElement("Button", "filterBtnIcon")
					:SetStyle("width", 18)
					:SetStyle("height", 18)
					:SetStyle("backgroundTexturePack", "iconPack.18x18/Chevron/Collapsed")
					-- TODO
					-- :SetScript("OnClick", private.FilterSearchButtonOnClick)
				)
			)
			:AddChild(TSMAPI_FOUR.UI.NewElement("Texture", "line")
				:SetStyle("height", 2)
				:SetStyle("color", "#9d9d9d")
			)
			:AddChild(TSMAPI_FOUR.UI.NewElement("ProfessionScrollingTable", "recipeList")
				:SetQuery(private.professionQuery)
				:SetScript("OnSelectionChanged", private.RecipeListOnSelectionChanged)
			)
			:AddChild(TSMAPI_FOUR.UI.NewElement("Text", "recipeListLoadingText")
				:SetStyle("justifyH", "CENTER")
				:SetText(L["Profession loading..."])
			)
		frame:GetElement("recipeList"):Hide()
		return frame
	elseif button == "group" then
		return TSMAPI_FOUR.UI.NewElement("Frame", "profession")
			:SetLayout("VERTICAL")
			:AddChild(TSMAPI_FOUR.UI.NewElement("SearchInput", "search")
				:SetStyle("height", 20)
				:SetStyle("margin", 8)
				:SetText(private.groupSearch)
				:SetHintText(L["Search Groups"])
				:SetScript("OnTextChanged", private.GroupSearchOnTextChanged)
			)
			:AddChild(TSMAPI_FOUR.UI.NewElement("Texture", "line")
				:SetStyle("height", 2)
				:SetStyle("color", "#9d9d9d")
			)
			:AddChild(TSMAPI_FOUR.UI.NewElement("ApplicationGroupTree", "groupTree")
				:SetGroupListFunc(private.GroupTreeGetList)
				:SetSearchString(private.groupSearch)
				:SetScript("OnGroupSelectionChanged", private.GroupTreeOnGroupSelectionChanged)
			)
			:AddChild(TSMAPI_FOUR.UI.NewElement("Texture", "line")
				:SetStyle("height", 2)
				:SetStyle("color", "#9d9d9d")
			)
			:AddChild(TSMAPI_FOUR.UI.NewElement("ActionButton", "addBtn")
				:SetStyle("width", 240)
				:SetStyle("height", 26)
				:SetStyle("margin", 8)
				:SetText(L["Add to Queue"])
				:SetDisabled(true)
				:SetScript("OnClick", private.QueueAddBtnOnClick)
			)
	else
		error("Unexpected button: "..tostring(button))
	end
end



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

function private.FrameOnUpdate(frame)
	-- delay the FSM event by a few frames to give textures a chance to load
	if private.showDelayFrame == SHOW_DELAY_FRAMES then
		frame:SetScript("OnUpdate", nil)
		private.fsm:ProcessEvent("EV_FRAME_SHOW", frame)
	else
		private.showDelayFrame = private.showDelayFrame + 1
	end
end

function private.FrameOnHide()
	private.showDelayFrame = 0
	private.fsm:ProcessEvent("EV_FRAME_HIDE")
end

function private.GroupSearchOnTextChanged(input)
	private.groupSearch = strlower(strtrim(input:GetText()))
	input:GetElement("__parent.groupTree")
		:SetSearchString(private.groupSearch)
		:Draw()
end

function private.GroupTreeGetList(groups, headerNameLookup)
	TSM.UI.ApplicationGroupTreeGetGroupList(groups, headerNameLookup, "Crafting")
end

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

function private.DropdownOnSelectionChanged(dropdown, selection)
	local key = TSMAPI_FOUR.Util.TableIndexOf(CRAFTING_PAGES, selection)
	assert(key)
	private.page = key
	dropdown:GetElement("__parent.__parent.content"):SetPath(key, true)
	private.fsm:ProcessEvent("EV_PAGE_CHANGED")
end

function private.ProfessionDropdownOnSelectionChanged(_, value)
	if not value then
		-- nothing selected
	else
		local key = TSMAPI_FOUR.Util.GetDistinctTableKey(private.professions, value)
		local player, profession = strsplit(KEY_SEP, key)
		if not profession then
			-- the current linked / guild / NPC profession was re-selected, so just ignore this change
			return
		end
		private.fsm:ProcessEvent("EV_CHANGE_PROFESSION", player, profession)
	end
end

function private.FilterSearchInputOnEnterPressed(input)
	private.professionQuery:Reset()
	local filter = strtrim(input:GetText())
	if filter ~= "" then
		private.professionQuery:Matches("name", TSMAPI_FOUR.Util.StrEscape(filter))
	end
	input:GetElement("__parent.__parent.recipeList"):SetQuery(private.professionQuery, true)
end

function private.RecipeListOnSelectionChanged(list)
	private.fsm:ProcessEvent("EV_RECIPE_SELECTION_CHANGED", list:GetSelection())
end

function private.QueueInputOnEnterPressed(input)
	local value = max(tonumber(input:GetText()) or 0, 1)
	input:SetText(value)
	input:Draw()
end

function private.MinusBtnOnClick(button)
	local input = button:GetParentElement():GetElement("queueInput")
	local value = max(tonumber(input:GetText()) or 0, 2) - 1
	input:SetText(value)
	input:Draw()
end

function private.PlusBtnOnClick(button)
	local input = button:GetParentElement():GetElement("queueInput")
	local value = max(tonumber(input:GetText()) or 0, 0) + 1
	input:SetText(value)
	input:Draw()
end

function private.QueueBtnOnClick(button)
	local value = max(tonumber(button:GetElement("__parent.queueInput"):GetText()) or 0, 1)
	private.fsm:ProcessEvent("EV_QUEUE_BUTTON_CLICKED", value)
	button:SetPressed(false)
	button:Draw()
end

function private.CraftBtnOnClick(button)
	local quantity = max(tonumber(button:GetElement("__parent.__parent.queue.queueInput"):GetText()) or 0, 1)
	private.fsm:ProcessEvent("EV_CRAFT_BUTTON_CLICKED", quantity)
end

function private.CraftAllBtnOnClick(button)
	private.fsm:ProcessEvent("EV_CRAFT_BUTTON_CLICKED", -1)
end

function private.QueueOnRowClick(button, data)
	local spellId = data:GetField("spellId")
	private.fsm:ProcessEvent("EV_CRAFT_NEXT_BUTTON_CLICKED", spellId, TSM.Crafting.Queue.GetNum(spellId))
end

function private.CraftNextOnClick(button)
	local spellId = button:GetElement("__parent.__parent.queueList"):GetFirstData():GetField("spellId")
	private.fsm:ProcessEvent("EV_CRAFT_NEXT_BUTTON_CLICKED", spellId, TSM.Crafting.Queue.GetNum(spellId))
end

function private.QueueAddBtnOnClick(button)
	local groups = TSMAPI_FOUR.Util.AcquireTempTable()
	for _, groupPath in button:GetElement("__parent.groupTree"):SelectedGroupsIterator() do
		tinsert(groups, groupPath)
	end
	TSM.Crafting.Queue.RestockGroups(groups)
	TSMAPI_FOUR.Util.ReleaseTempTable(groups)
	button:SetPressed(false)
	button:Draw()
end



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

function private.FSMCreate()
	local function OnTradeSkillClose()
		private.fsm:ProcessEvent("EV_TRADE_SKILL_CLOSED")
	end
	TSMAPI_FOUR.Event.Register("TRADE_SKILL_CLOSE", OnTradeSkillClose)
	TSMAPI_FOUR.Event.Register("GARRISON_TRADESKILL_NPC_CLOSE", OnTradeSkillClose)
	TSMAPI_FOUR.Event.Register("TRADE_SKILL_DATA_SOURCE_CHANGING", function()
		private.fsm:ProcessEvent("EV_DATA_SOURCE_CHANGING")
	end)
	TSMAPI_FOUR.Event.Register("UPDATE_TRADESKILL_RECAST", function()
		private.fsm:ProcessEvent("EV_UPDATE_TRADESKILL_RECAST")
	end)
	TSMAPI_FOUR.Event.Register("UNIT_SPELLCAST_SUCCEEDED", function(_, unit, _, _, _, spellId)
		if unit ~= "player" then
			return
		end
		private.fsm:ProcessEvent("EV_SPELLCAST_COMPLETE", spellId, true)
	end)
	local function SpellcastFailedEventHandler(_, unit, _, _, _, spellId)
		if unit ~= "player" then
			return
		end
		private.fsm:ProcessEvent("EV_SPELLCAST_COMPLETE", spellId, false)
	end
	TSMAPI_FOUR.Event.Register("UNIT_SPELLCAST_INTERRUPTED", SpellcastFailedEventHandler)
	TSMAPI_FOUR.Event.Register("UNIT_SPELLCAST_FAILED", SpellcastFailedEventHandler)
	TSMAPI_FOUR.Event.Register("UNIT_SPELLCAST_FAILED_QUIET", SpellcastFailedEventHandler)

	local function QueryUpdateCallback()
		private.fsm:ProcessEvent("EV_QUERY_CHANGED")
	end
	local fsmContext = {
		frame = nil,
		selectedRecipeSpellId = nil,
		craftingSpellId = nil,
		craftingQuantity = nil,
		craftingType = nil,
		scannerQuery = nil,
		queueQuery = nil,
		currentProfession = nil,
	}
	local function UpdateCraftingFrame(context)
		if not context.frame then
			return
		end

		local dropdownSelection = nil
		local _, currentProfession = C_TradeSkillUI.GetTradeSkillLine()
		local isCurrentProfessionPlayer = not C_TradeSkillUI.IsNPCCrafting() and not C_TradeSkillUI.IsTradeSkillLinked() and not C_TradeSkillUI.IsTradeSkillGuild()
		wipe(private.professions)
		wipe(private.professionsOrder)
		if currentProfession and not isCurrentProfessionPlayer then
			local playerName = nil
			local linked, linkedName = C_TradeSkillUI.IsTradeSkillLinked()
			if linked then
				playerName = linkedName or "?"
			elseif C_TradeSkillUI.IsNPCCrafting() then
				playerName = L["NPC"]
			elseif C_TradeSkillUI.IsTradeSkillGuild() then
				playerName = L["Guild"]
			end
			assert(playerName)
			local key = currentProfession
			tinsert(private.professionsOrder, key)
			private.professions[key] = format("%s - %s", currentProfession, playerName)
			dropdownSelection = key
		end
		local query = TSM.Crafting.PlayerProfessions.GetQuery()
			:Select("player", "profession", "level", "maxLevel")
			-- TODO: support showing of other player's professions?
			:Equal("player", UnitName("player"))
			:OrderBy("isSecondary", true)
			:OrderBy("level", false)
			:OrderBy("profession", true)
		for _, player, profession, level, maxLevel in query:Iterator(true) do
			local key = player..KEY_SEP..profession
			tinsert(private.professionsOrder, key)
			private.professions[key] = format("%s %d/%d - %s", profession, level, maxLevel, player)
			if isCurrentProfessionPlayer and profession == currentProfession then
				assert(not dropdownSelection)
				dropdownSelection = key
			end
		end

		local recipeList = nil
		if private.page == "profession" then
			local craftingContentFrame = context.frame:GetElement("left.content.profession")
			craftingContentFrame:GetElement("professionDropdown")
				:SetDictionaryItems(private.professions, private.professions[dropdownSelection], private.professionsOrder)
			recipeList = craftingContentFrame:GetElement("recipeList")
			if not currentProfession then
				recipeList:Hide()
				local text = craftingContentFrame:GetElement("recipeListLoadingText")
				text:SetText(L["No Profession Selected"])
				text:Show()
			elseif not C_TradeSkillUI.IsTradeSkillReady() or C_TradeSkillUI.IsDataSourceChanging() then
				recipeList:Hide()
				local text = craftingContentFrame:GetElement("recipeListLoadingText")
				text:SetText(L["Loading..."])
				text:Show()
			else
				recipeList:Show()
				craftingContentFrame:GetElement("recipeListLoadingText"):Hide()
			end
		end

		local detailsFrame = context.frame:GetElement("right.details")
		local queueFrame = context.frame:GetElement("right.queue")
		if private.page == "profession" and context.selectedRecipeSpellId and C_TradeSkillUI.IsTradeSkillReady() and not C_TradeSkillUI.IsDataSourceChanging() then
			if recipeList:GetSelection() ~= context.selectedRecipeSpellId then
				recipeList:SetSelection(context.selectedRecipeSpellId)
			end
			local resultName, resultItemString = TSM.Crafting.ProfessionUtil.GetResultInfo(context.selectedRecipeSpellId)
			detailsFrame:GetElement("title.name"):SetText(resultName)
			detailsFrame:GetElement("title.name"):SetTooltip(resultItemString)
			local toolsStr, hasTools = C_TradeSkillUI.GetRecipeTools(context.selectedRecipeSpellId)
			local errorText = detailsFrame:GetElement("reagentsTitle.error")
			local canCraft = false
			if toolsStr and not hasTools then
				errorText:SetText("("..REQUIRES_LABEL..toolsStr..")")
			elseif (not toolsStr or hasTools) and TSM.Crafting.ProfessionUtil.GetNumCraftable(context.selectedRecipeSpellId) == 0 then
				errorText:SetText("("..L["Missing Materials"]..")")
			else
				canCraft = true
				errorText:SetText("")
			end
			local isEnchant = TSM.Crafting.ProfessionUtil.IsEnchant(context.selectedRecipeSpellId)
			detailsFrame:GetElement("craft.craftBtn")
				:SetDisabled(not canCraft or context.craftingSpellId)
				:SetPressed(context.craftingSpellId and context.craftingType == "craft")
			detailsFrame:GetElement("craft.craftAllBtn")
				:SetText(isEnchant and L["Enchant Vellum"] or L["Craft All"])
				:SetDisabled(not canCraft or context.craftingSpellId)
				:SetPressed(context.craftingSpellId and context.craftingType == "all")
			local rankFrame = detailsFrame:GetElement("title.rank")
			local rank = TSM.Crafting.ProfessionScanner.GetRankBySpellId(context.selectedRecipeSpellId)
			assert(rank)
			if rank == -1 then
				rankFrame:Hide()
			else
				local filledTexture, unfilledTexture = "iconPack.10x10/Star/Filled", "iconPack.10x10/Star/Unfilled"
				rankFrame:GetElement("star1")
					:SetStyle("texturePack", rank >= 1 and filledTexture or unfilledTexture)
					:SetStyle("vertexColor", rank >= 1 and "#ffd839" or "#e2e2e2")
				rankFrame:GetElement("star2")
					:SetStyle("texturePack", rank >= 2 and filledTexture or unfilledTexture)
					:SetStyle("vertexColor", rank >= 2 and "#ffd839" or "#e2e2e2")
				rankFrame:GetElement("star3")
					:SetStyle("texturePack", rank >= 3 and filledTexture or unfilledTexture)
					:SetStyle("vertexColor", rank >= 3 and "#ffd839" or "#e2e2e2")
				rankFrame:Show()
			end
			detailsFrame:GetElement("matList"):SetRecipe(context.selectedRecipeSpellId)
			detailsFrame:Show()
			queueFrame:_GetStyle("padding").top = 6
		else
			if private.page == "profession" and recipeList:GetSelection() then
				recipeList:SetSelection(nil)
			end
			detailsFrame:Hide()
			queueFrame:_GetStyle("padding").top = 35
		end

		local totalCost, totalProfit = TSM.Crafting.Queue.GetTotalCostAndProfit()
		local totalCostText = totalCost and TSMAPI_FOUR.Money.ToString(totalCost, totalCost >= 0 and "|cff2cec0d" or "|cffd50000") or ""
		queueFrame:GetElement("queueCost.text"):SetText(totalCostText)
		local totalProfitText = totalProfit and TSMAPI_FOUR.Money.ToString(totalProfit, totalProfit >= 0 and "|cff2cec0d" or "|cffd50000") or ""
		queueFrame:GetElement("queueProfit.text"):SetText(totalProfitText)
		queueFrame:GetElement("queueList"):Draw()
		local nextCraftRecord = queueFrame:GetElement("queueList"):GetFirstData()
		if nextCraftRecord and (TSM.Crafting.GetProfession(nextCraftRecord:GetField("spellId")) ~= context.currentProfession or TSM.Crafting.ProfessionUtil.GetNumCraftable(nextCraftRecord:GetField("spellId")) == 0) then
			nextCraftRecord = nil
		end
		local canCraftFromQueue = currentProfession and isCurrentProfessionPlayer and C_TradeSkillUI.IsTradeSkillReady() and not C_TradeSkillUI.IsDataSourceChanging()
		queueFrame:GetElement("craft.craftNextBtn")
			:SetDisabled(not canCraftFromQueue or not nextCraftRecord or context.craftingSpellId)
			:SetPressed(context.craftingSpellId and context.craftingType == "queue")
		if nextCraftRecord and canCraftFromQueue then
			C_TradeSkillUI.SetRecipeRepeatCount(nextCraftRecord:GetField("spellId"), nextCraftRecord:GetField("num"))
		end

		context.frame:Draw()
	end
	private.fsm = TSMAPI_FOUR.FSM.New("CRAFTING_UI_CRAFTING")
		:AddState(TSMAPI_FOUR.FSM.NewState("ST_FRAME_CLOSED")
			:SetOnEnter(function(context)
				context.selectedRecipeSpellId = nil
				context.frame = nil
				context.currentProfession = nil
				if context.scannerQuery then
					context.scannerQuery:Release()
					context.scannerQuery = nil
				end
			end)
			:AddTransition("ST_FRAME_CLOSED")
			:AddTransition("ST_FRAME_OPENING")
			:AddEvent("EV_FRAME_SHOW", TSMAPI_FOUR.FSM.SimpleTransitionEventHandler("ST_FRAME_OPENING"))
		)
		:AddState(TSMAPI_FOUR.FSM.NewState("ST_FRAME_OPENING")
			:SetOnEnter(function(context, frame)
				local _, currentProfession = C_TradeSkillUI.GetTradeSkillLine()
				if currentProfession ~= context.currentProfession then
					context.selectedRecipeSpellId = TSM.Crafting.ProfessionScanner.GetFirstSpellId()
					context.currentProfession = currentProfession
				end
				if not context.queueQuery then
					context.queueQuery = TSM.Crafting.Queue.CreateQuery()
					context.queueQuery:SetUpdateCallback(QueryUpdateCallback)
				end
				context.scannerQuery = TSM.Crafting.ProfessionScanner.CreateQuery()
				context.scannerQuery:SetUpdateCallback(QueryUpdateCallback)
				context.frame = frame
				TSM:LOG_INFO("Selected %s", context.selectedRecipeSpellId)
				return "ST_FRAME_OPEN"
			end)
			:AddTransition("ST_FRAME_OPEN")
		)
		:AddState(TSMAPI_FOUR.FSM.NewState("ST_FRAME_OPEN")
			:SetOnEnter(function(context)
				if context.currentProfession and not context.selectedRecipeSpellId then
					context.selectedRecipeSpellId = TSM.Crafting.ProfessionScanner.GetFirstSpellId()
				end
				UpdateCraftingFrame(context)
			end)
			:AddTransition("ST_FRAME_CLOSED")
			:AddTransition("ST_FRAME_OPEN")
			:AddTransition("ST_CHANGING_SELECTION")
			:AddTransition("ST_STARTING_CRAFT")
			:AddTransition("ST_CASTING_COMPLETE")
			:AddEvent("EV_PAGE_CHANGED", TSMAPI_FOUR.FSM.SimpleTransitionEventHandler("ST_FRAME_OPEN"))
			:AddEvent("EV_DATA_SOURCE_CHANGING", function(context, selection)
				context.selectedRecipeSpellId = nil
				return "ST_FRAME_OPEN"
			end)
			:AddEvent("EV_QUERY_CHANGED", TSMAPI_FOUR.FSM.SimpleTransitionEventHandler("ST_FRAME_OPEN"))
			:AddEvent("EV_TRADE_SKILL_CLOSED", function(context, selection)
				context.selectedRecipeSpellId = nil
				TSM:LOG_INFO("Selected %s", context.selectedRecipeSpellId)
				return "ST_FRAME_OPEN"
			end)
			:AddEvent("EV_CHANGE_PROFESSION", function(context, player, profession)
				context.selectedRecipeSpellId = nil
				TSM:LOG_INFO("Selected %s", context.selectedRecipeSpellId)
				-- TODO: support showing of other player's professions?
				assert(player == UnitName("player"))
				if profession == GetSpellInfo(MINING_SPELLID) then
					-- mining needs to be opened as smelting
					CastSpellByName(GetSpellInfo(SMELTING_SPELLID))
				else
					CastSpellByName(profession)
				end
				return "ST_FRAME_OPEN"
			end)
			:AddEvent("EV_RECIPE_SELECTION_CHANGED", TSMAPI_FOUR.FSM.SimpleTransitionEventHandler("ST_CHANGING_SELECTION"))
			:AddEvent("EV_CRAFT_BUTTON_CLICKED", function(context, quantity)
				context.craftingType = quantity == -1 and "all" or "craft"
				return "ST_STARTING_CRAFT", context.selectedRecipeSpellId, quantity
			end)
			:AddEvent("EV_CRAFT_NEXT_BUTTON_CLICKED", function(context, spellId, quantity)
				if TSM.Crafting.GetProfession(spellId) ~= context.currentProfession or TSM.Crafting.ProfessionUtil.GetNumCraftable(spellId) == 0 then
					context.frame:GetElement("right.details.craft.craftBtn"):SetPressed(false)
					return
				end
				context.craftingType = "queue"
				return "ST_STARTING_CRAFT", spellId, quantity
			end)
			:AddEvent("EV_SPELLCAST_COMPLETE", function(context, spellId, success)
				if context.craftingSpellId ~= spellId then
					return
				end
				return "ST_CASTING_COMPLETE", spellId, success
			end)
			:AddEvent("EV_QUEUE_BUTTON_CLICKED", function(context, quantity)
				assert(context.selectedRecipeSpellId)
				TSM.Crafting.Queue.Add(context.selectedRecipeSpellId, quantity)
				return "ST_FRAME_OPEN"
			end)
			:AddEvent("EV_UPDATE_TRADESKILL_RECAST", function(context)
				if context.craftingSpellId then
					C_TradeSkillUI.SetRecipeRepeatCount(context.craftingSpellId, context.craftingQuantity)
				end
			end)
		)
		:AddState(TSMAPI_FOUR.FSM.NewState("ST_CHANGING_SELECTION")
			:SetOnEnter(function(context, selection)
				context.selectedRecipeSpellId = selection
				TSM:LOG_INFO("Selected %s", context.selectedRecipeSpellId)
				return "ST_FRAME_OPEN"
			end)
			:AddTransition("ST_FRAME_OPEN")
		)
		:AddState(TSMAPI_FOUR.FSM.NewState("ST_STARTING_CRAFT")
			:SetOnEnter(function(context, spellId, quantity)
				TSM:LOG_INFO("Crafting %d of %d", quantity, spellId)
				if quantity == -1 then
					quantity = TSM.Crafting.ProfessionUtil.GetNumCraftable(spellId)
					if quantity == 0 then
						return "ST_FRAME_OPEN"
					end
				end
				context.craftingAll = quantity == -1
				local isEnchant = TSM.Crafting.ProfessionUtil.IsEnchant(spellId)
				if isEnchant then
					quantity = 1
				end
				if context.craftingType == "craft" then
					-- FIXME: since we don't have the repeat count set, we can only craft one due to blizzard weirdness
					quantity = 1
				end
				C_TradeSkillUI.CraftRecipe(spellId, quantity)
				if isEnchant and context.craftingType ~= "craft" then
					UseItemByName(TSMAPI_FOUR.Item.GetName(TSM.CONST.VELLUM_ITEM_STRING))
				end
				context.craftingSpellId = spellId
				context.craftingQuantity = quantity
				return "ST_FRAME_OPEN"
			end)
			:AddTransition("ST_FRAME_OPEN")
		)
		:AddState(TSMAPI_FOUR.FSM.NewState("ST_CASTING_COMPLETE")
			:SetOnEnter(function(context, spellId, success)
				if success then
					TSM:LOG_INFO("Crafted %d", spellId)
					TSM.Crafting.Queue.Remove(context.craftingSpellId, 1)
					context.craftingQuantity = context.craftingQuantity - 1
					assert(context.craftingQuantity >= 0)
					if context.craftingQuantity == 0 then
						context.craftingSpellId = nil
						context.craftingQuantity = nil
						context.craftingType = nil
					end
				else
					context.craftingSpellId = nil
					context.craftingQuantity = nil
					context.craftingType = nil
				end
				return "ST_FRAME_OPEN"
			end)
			:AddTransition("ST_FRAME_OPEN")
		)
		:AddDefaultEvent("EV_FRAME_HIDE", TSMAPI_FOUR.FSM.SimpleTransitionEventHandler("ST_FRAME_CLOSED"))
		:AddDefaultEvent("EV_SPELLCAST_COMPLETE", function(context, spellId)
			if context.craftingSpellId == spellId then
				context.craftingSpellId = nil
				context.craftingQuantity = nil
				context.craftingType = nil
			end
		end)
		:Init("ST_FRAME_CLOSED", fsmContext)
end
