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

local _, TSM = ...
local ProfessionUtil = TSM.Crafting:NewPackage("ProfessionUtil")
local ProfessionInfo = TSM.Include("Data.ProfessionInfo")
local Event = TSM.Include("Util.Event")
local TempTable = TSM.Include("Util.TempTable")
local Log = TSM.Include("Util.Log")
local Delay = TSM.Include("Util.Delay")
local ItemString = TSM.Include("Util.ItemString")
local ItemInfo = TSM.Include("Service.ItemInfo")
local BagTracking = TSM.Include("Service.BagTracking")
local Inventory = TSM.Include("Service.Inventory")
local CustomPrice = TSM.Include("Service.CustomPrice")
local private = {
	craftQuantity = nil,
	craftSpellId = nil,
	craftCallback = nil,
	craftName = nil,
	castingTimeout = nil,
	craftTimeout = nil,
	preparedSpellId = nil,
	preparedTime = 0,
	categoryInfoTemp = {},
}
local PROFESSION_LOOKUP = {
	["Costura"] = "Sastrería",
	["Marroquinería"] = "Peletería",
	["Ingénierie"] = "Ingénieur",
	["Secourisme"] = "Premiers soins",
}



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

function ProfessionUtil.OnInitialize()
	Event.Register("UNIT_SPELLCAST_SUCCEEDED", function(_, unit, _, spellId)
		if unit ~= "player" then
			return
		end
		if (TSM.IsWowClassic() and GetSpellInfo(spellId) ~= private.craftName) or (not TSM.IsWowClassic() and spellId ~= private.craftSpellId) then
			return
		end

		-- check if we need to update bank quantity manually
		for _, itemString, quantity in TSM.Crafting.MatIterator(private.craftSpellId) do
			local bankUsed = quantity - (Inventory.GetBagQuantity(itemString) + Inventory.GetReagentBankQuantity(itemString))
			if bankUsed > 0 and bankUsed <= Inventory.GetBankQuantity(itemString) then
				Log.Info("Used %d from bank", bankUsed)
				BagTracking.ForceBankQuantityDeduction(itemString, bankUsed)
			end
		end

		local callback = private.craftCallback
		assert(callback)
		private.craftQuantity = private.craftQuantity - 1
		private.DoCraftCallback(true, private.craftQuantity == 0)
		-- ignore profession updates from crafting something
		TSM.Crafting.ProfessionScanner.IgnoreNextProfessionUpdates()
		-- restart the timeout
	end)
	local function SpellcastFailedEventHandler(_, unit, _, spellId)
		if unit ~= "player" then
			return
		end
		if (TSM.IsWowClassic() and GetSpellInfo(spellId) ~= private.craftName) or (not TSM.IsWowClassic() and spellId ~= private.craftSpellId) then
			return
		end
		private.DoCraftCallback(false, true)
	end
	local function ClearCraftCast()
		private.craftQuantity = nil
		private.craftSpellId = nil
		private.craftName = nil
		private.castingTimeout = nil
		private.craftTimeout = nil
	end
	Event.Register("UNIT_SPELLCAST_INTERRUPTED", SpellcastFailedEventHandler)
	Event.Register("UNIT_SPELLCAST_FAILED", SpellcastFailedEventHandler)
	Event.Register("UNIT_SPELLCAST_FAILED_QUIET", SpellcastFailedEventHandler)
	Event.Register("TRADE_SKILL_CLOSE", ClearCraftCast)
	if TSM.IsWowClassic() then
		Event.Register("CRAFT_CLOSE", ClearCraftCast)
	end
end

function ProfessionUtil.GetCurrentProfessionName()
	if TSM.IsWowClassic() then
		local name = TSM.Crafting.ProfessionState.IsClassicCrafting() and GetCraftSkillLine(1) or GetTradeSkillLine()
		return name
	else
		local _, name, _, _, _, _, parentName = C_TradeSkillUI.GetTradeSkillLine()
		return parentName or name
	end
end

function ProfessionUtil.GetResultInfo(spellId)
	-- get the links
	local itemLink = ProfessionUtil.GetRecipeInfo(spellId)
	assert(itemLink, "Invalid craft: "..tostring(spellId))

	if strfind(itemLink, "enchant:") then
		-- result of craft is not an item
		local itemString = ProfessionInfo.GetIndirectCraftResult(spellId)
		if itemString and not TSM.IsWowClassic() then
			return TSM.UI.GetColoredItemName(itemString), itemString, ItemInfo.GetTexture(itemString)
		elseif ProfessionInfo.IsEngineeringTinker(spellId) then
			local name, _, icon = GetSpellInfo(spellId)
			return name, nil, icon
		else
			local name, _, icon = GetSpellInfo(TSM.Crafting.ProfessionState.IsClassicCrafting() and GetCraftInfo(TSM.IsWowClassic() and TSM.Crafting.ProfessionScanner.GetIndexBySpellId(spellId) or spellId) or spellId)
			return name, nil, icon
		end
	elseif strfind(itemLink, "item:") then
		-- result of craft is an item
		return TSM.UI.GetColoredItemName(itemLink), ItemString.Get(itemLink), ItemInfo.GetTexture(itemLink)
	else
		error("Invalid craft: "..tostring(spellId))
	end
end

function ProfessionUtil.GetNumCraftable(spellId)
	local num, numAll = math.huge, math.huge
	for i = 1, ProfessionUtil.GetNumMats(spellId) do
		local matItemLink, _, _, quantity = ProfessionUtil.GetMatInfo(spellId, i)
		local itemString = ItemString.Get(matItemLink)
		local totalQuantity = CustomPrice.GetItemPrice(itemString, "NumInventory") or 0
		if not itemString or not quantity or totalQuantity == 0 then
			return 0, 0
		end
		local bagQuantity = Inventory.GetBagQuantity(itemString)
		if not TSM.IsWowClassic() then
			bagQuantity = bagQuantity + Inventory.GetReagentBankQuantity(itemString) + Inventory.GetBankQuantity(itemString)
		end
		num = min(num, floor(bagQuantity / quantity))
		numAll = min(numAll, floor(totalQuantity / quantity))
	end
	if num == math.huge or numAll == math.huge then
		return 0, 0
	end
	return num, numAll
end

function ProfessionUtil.IsCraftable(spellId)
	for i = 1, ProfessionUtil.GetNumMats(spellId) do
		local matItemLink, _, _, quantity = ProfessionUtil.GetMatInfo(spellId, i)
		local itemString = ItemString.Get(matItemLink)
		if not itemString or not quantity then
			return false
		end
		local bagQuantity = Inventory.GetBagQuantity(itemString)
		if not TSM.IsWowClassic() then
			bagQuantity = bagQuantity + Inventory.GetReagentBankQuantity(itemString) + Inventory.GetBankQuantity(itemString)
		end
		if floor(bagQuantity / quantity) == 0 then
			return false
		end
	end
	return true
end

function ProfessionUtil.GetNumCraftableFromDB(spellId)
	local num = math.huge
	for _, itemString, quantity in TSM.Crafting.MatIterator(spellId) do
		local bagQuantity = Inventory.GetBagQuantity(itemString)
		if not TSM.IsWowClassic() then
			bagQuantity = bagQuantity + Inventory.GetReagentBankQuantity(itemString) + Inventory.GetBankQuantity(itemString)
		end
		num = min(num, floor(bagQuantity / quantity))
	end
	if num == math.huge then
		return 0
	end
	return num
end

function ProfessionUtil.IsEnchant(spellId)
	local name = ProfessionUtil.GetCurrentProfessionName()
	if name ~= GetSpellInfo(7411) or TSM.IsWowClassic() then
		return false
	end
	if not strfind(C_TradeSkillUI.GetRecipeItemLink(spellId), "enchant:") then
		return false
	end
	local recipeInfo = nil
	if not TSM.IsShadowlands() then
		recipeInfo = TempTable.Acquire()
		assert(C_TradeSkillUI.GetRecipeInfo(spellId, recipeInfo) == recipeInfo)
	else
		recipeInfo = C_TradeSkillUI.GetRecipeInfo(spellId)
	end
	local altVerb = recipeInfo.alternateVerb
	if not TSM.IsShadowlands() then
		TempTable.Release(recipeInfo)
	end
	return altVerb and true or false
end

function ProfessionUtil.OpenProfession(profession, skillId)
	if TSM.IsWowClassic() then
		if profession == ProfessionInfo.GetName("Mining") then
			-- mining needs to be opened as smelting
			profession = ProfessionInfo.GetName("Smelting")
		end
		if PROFESSION_LOOKUP[profession] then
			profession = PROFESSION_LOOKUP[profession]
		end
		CastSpellByName(profession)
	else
		C_TradeSkillUI.OpenTradeSkill(skillId)
	end
end

function ProfessionUtil.PrepareToCraft(spellId, quantity)
	quantity = min(quantity, ProfessionUtil.GetNumCraftable(spellId))
	if quantity == 0 then
		return
	end
	if ProfessionUtil.IsEnchant(spellId) then
		quantity = 1
	end

	if not TSM.IsWowClassic() then
		C_TradeSkillUI.SetRecipeRepeatCount(spellId, quantity)
	end
	private.preparedSpellId = spellId
	private.preparedTime = GetTime()
end

function ProfessionUtil.Craft(spellId, quantity, useVellum, callback)
	assert(TSM.Crafting.ProfessionScanner.HasSpellId(spellId))
	if private.craftSpellId then
		private.craftCallback = callback
		private.DoCraftCallback(false, true)
		return 0
	end
	quantity = min(quantity, ProfessionUtil.GetNumCraftable(spellId))
	if quantity == 0 then
		return 0
	end
	local isEnchant = ProfessionUtil.IsEnchant(spellId)
	if isEnchant then
		quantity = 1
	elseif spellId ~= private.preparedSpellId or private.preparedTime == GetTime() then
		-- We can only craft one of this item due to a bug on Blizzard's end
		quantity = 1
	end
	private.craftQuantity = quantity
	private.craftSpellId = spellId
	private.craftCallback = callback
	if TSM.IsWowClassic() then
		spellId = TSM.Crafting.ProfessionScanner.GetIndexBySpellId(spellId)
		if TSM.Crafting.ProfessionState.IsClassicCrafting() then
			private.craftName = GetCraftInfo(spellId)
		else
			private.craftName = GetTradeSkillInfo(spellId)
			DoTradeSkill(spellId, quantity)
		end
	else
		C_TradeSkillUI.CraftRecipe(spellId, quantity)
	end
	if useVellum and isEnchant then
		UseItemByName(ItemInfo.GetName(ProfessionInfo.GetVellumItemString()))
	end
	private.castingTimeout = nil
	private.craftTimeout = nil
	Delay.AfterTime("PROFESSION_CRAFT_TIMEOUT_MONITOR", 0.5, private.CraftTimeoutMonitor, 0.5)
	return quantity
end

function ProfessionUtil.IsDataStable()
	return TSM.IsWowClassic() or (C_TradeSkillUI.IsTradeSkillReady() and not C_TradeSkillUI.IsDataSourceChanging())
end

function ProfessionUtil.HasCooldown(spellId)
	if TSM.IsWowClassic() then
		return GetTradeSkillCooldown(spellId) and true or false
	else
		return select(2, C_TradeSkillUI.GetRecipeCooldown(spellId)) and true or false
	end
end

function ProfessionUtil.GetRemainingCooldown(spellId)
	if TSM.IsWowClassic() then
		return GetTradeSkillCooldown(spellId)
	else
		return C_TradeSkillUI.GetRecipeCooldown(spellId)
	end
end

function ProfessionUtil.GetRecipeInfo(spellId)
	local itemLink, lNum, hNum, toolsStr, hasTools = nil, nil, nil, nil, nil
	if TSM.IsWowClassic() then
		spellId = TSM.Crafting.ProfessionScanner.GetIndexBySpellId(spellId) or spellId
		itemLink = TSM.Crafting.ProfessionState.IsClassicCrafting() and GetCraftItemLink(spellId) or GetTradeSkillItemLink(spellId)
		if TSM.Crafting.ProfessionState.IsClassicCrafting() then
			lNum, hNum = 1, 1
			toolsStr, hasTools = GetCraftSpellFocus(spellId)
		else
			lNum, hNum = GetTradeSkillNumMade(spellId)
			toolsStr, hasTools = GetTradeSkillTools(spellId)
		end
	else
		itemLink = C_TradeSkillUI.GetRecipeItemLink(spellId)
		lNum, hNum = C_TradeSkillUI.GetRecipeNumItemsProduced(spellId)
		toolsStr, hasTools = C_TradeSkillUI.GetRecipeTools(spellId)
	end
	return itemLink, lNum, hNum, toolsStr, hasTools
end

function ProfessionUtil.GetNumMats(spellId)
	local numMats = nil
	if TSM.IsWowClassic() then
		spellId = TSM.Crafting.ProfessionScanner.GetIndexBySpellId(spellId) or spellId
		numMats = TSM.Crafting.ProfessionState.IsClassicCrafting() and GetCraftNumReagents(spellId) or GetTradeSkillNumReagents(spellId)
	else
		numMats = C_TradeSkillUI.GetRecipeNumReagents(spellId)
	end
	return numMats
end

function ProfessionUtil.GetMatInfo(spellId, index)
	local itemLink, name, texture, quantity = nil, nil, nil, nil
	if TSM.IsWowClassic() then
		spellId = TSM.Crafting.ProfessionScanner.GetIndexBySpellId(spellId) or spellId
		itemLink = TSM.Crafting.ProfessionState.IsClassicCrafting() and GetCraftReagentItemLink(spellId, index) or GetTradeSkillReagentItemLink(spellId, index)
		if TSM.Crafting.ProfessionState.IsClassicCrafting() then
			name, texture, quantity = GetCraftReagentInfo(spellId, index)
		else
			name, texture, quantity = GetTradeSkillReagentInfo(spellId, index)
		end
	else
		itemLink = C_TradeSkillUI.GetRecipeReagentItemLink(spellId, index)
		name, texture, quantity = C_TradeSkillUI.GetRecipeReagentInfo(spellId, index)
		if itemLink then
			name = name or ItemInfo.GetName(itemLink)
			texture = texture or ItemInfo.GetTexture(itemLink)
		end
	end
	return itemLink, name, texture, quantity
end

function ProfessionUtil.CloseTradeSkill(closeBoth)
	if TSM.IsWowClassic() then
		if closeBoth then
			CloseCraft()
			CloseTradeSkill()
		else
			if TSM.Crafting.ProfessionState.IsClassicCrafting() then
				CloseCraft()
			else
				CloseTradeSkill()
			end
		end
	else
		C_TradeSkillUI.CloseTradeSkill()
		C_Garrison.CloseGarrisonTradeskillNPC()
	end
end

function ProfessionUtil.IsNPCProfession()
	return not TSM.IsWowClassic() and C_TradeSkillUI.IsNPCCrafting()
end

function ProfessionUtil.IsLinkedProfession()
	if TSM.IsWowClassic() then
		return nil, nil
	else
		return C_TradeSkillUI.IsTradeSkillLinked()
	end
end

function ProfessionUtil.IsGuildProfession()
	return not TSM.IsWowClassic() and C_TradeSkillUI.IsTradeSkillGuild()
end

function ProfessionUtil.GetCategoryInfo(categoryId)
	local name, numIndents, parentCategoryId, currentSkillLevel, maxSkillLevel = nil, nil, nil, nil, nil
	if TSM.IsWowClassic() then
		name = TSM.Crafting.ProfessionState.IsClassicCrafting() and GetCraftDisplaySkillLine() or (categoryId and GetTradeSkillInfo(categoryId) or nil)
		numIndents = 0
		parentCategoryId = nil
	else
		C_TradeSkillUI.GetCategoryInfo(categoryId, private.categoryInfoTemp)
		assert(private.categoryInfoTemp.numIndents)
		name = private.categoryInfoTemp.name
		numIndents = private.categoryInfoTemp.numIndents
		parentCategoryId = private.categoryInfoTemp.numIndents ~= 0 and private.categoryInfoTemp.parentCategoryID or nil
		currentSkillLevel = private.categoryInfoTemp.skillLineCurrentLevel
		maxSkillLevel = private.categoryInfoTemp.skillLineMaxLevel
		wipe(private.categoryInfoTemp)
	end
	return name, numIndents, parentCategoryId, currentSkillLevel, maxSkillLevel
end

function ProfessionUtil.StoreOptionalMatText(matList, text)
	TSM.db.global.internalData.optionalMatTextLookup[matList] = TSM.db.global.internalData.optionalMatTextLookup[matList] or text
end

function ProfessionUtil.GetOptionalMatText(matList)
	return TSM.db.global.internalData.optionalMatTextLookup[matList]
end



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

function private.DoCraftCallback(result, isDone)
	local callback = private.craftCallback
	assert(callback)
	-- reset timeouts
	private.castingTimeout = nil
	private.craftTimeout = nil
	if isDone then
		private.craftQuantity = nil
		private.craftSpellId = nil
		private.craftCallback = nil
		private.craftName = nil
		Delay.Cancel("PROFESSION_CRAFT_TIMEOUT_MONITOR")
	end
	callback(result, isDone)
end

function private.CraftTimeoutMonitor()
	if not private.craftSpellId then
		Log.Info("No longer crafting")
		private.castingTimeout = nil
		private.craftTimeout = nil
		Delay.Cancel("PROFESSION_CRAFT_TIMEOUT_MONITOR")
		return
	end
	local _, _, _, _, castEndTimeMs, _, _, _, spellId = private.GetPlayerCastingInfo()
	if spellId then
		private.castingTimeout = nil
	else
		private.craftTimeout = nil
	end
	if not spellId then
		-- no active cast
		if GetTime() > (private.castingTimeout or math.huge) then
			Log.Err("Craft timed out (%s)", private.craftSpellId)
			private.DoCraftCallback(false, true)
			return
		end
		-- set the casting timeout to 1 second from now
		private.castingTimeout = GetTime() + 1
		return
	elseif private.craftSpellId ~= spellId then
		Log.Err("Crafting something else (%s, %s)", private.craftSpellId, spellId)
		private.castingTimeout = nil
		private.craftTimeout = nil
		Delay.Cancel("PROFESSION_CRAFT_TIMEOUT_MONITOR")
		return
	end

	if GetTime() > (private.craftTimeout or math.huge) then
		Log.Err("Craft timed out (%s)", private.craftSpellId)
		private.DoCraftCallback(false, true)
		return
	end
	-- set the timeout to 1 second after the end time
	private.craftTimeout = castEndTimeMs / 1000 + 1
end

function private.GetPlayerCastingInfo()
	if TSM.IsWowClassic() then
		return CastingInfo()
	else
		return UnitCastingInfo("player")
	end
end
