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

local _, TSM = ...
local Gathering = TSM.Crafting:NewPackage("Gathering")
local DisenchantInfo = TSM.Include("Data.DisenchantInfo")
local Database = TSM.Include("Util.Database")
local Table = TSM.Include("Util.Table")
local Delay = TSM.Include("Util.Delay")
local String = TSM.Include("Util.String")
local TempTable = TSM.Include("Util.TempTable")
local ItemInfo = TSM.Include("Service.ItemInfo")
local Conversions = TSM.Include("Service.Conversions")
local BagTracking = TSM.Include("Service.BagTracking")
local Inventory = TSM.Include("Service.Inventory")
local PlayerInfo = TSM.Include("Service.PlayerInfo")
local private = {
	db = nil,
	queuedCraftsUpdateQuery = nil, -- luacheck: ignore 1004 - just stored for GC reasons
	crafterList = {},
	professionList = {},
	contextChangedCallback = nil,
}



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

function Gathering.OnInitialize()
	if TSM.IsWowClassic() then
		Table.RemoveByValue(TSM.db.profile.gatheringOptions.sources, "guildBank")
		Table.RemoveByValue(TSM.db.profile.gatheringOptions.sources, "altGuildBank")
	end
end

function Gathering.OnEnable()
	private.db = Database.NewSchema("GATHERING_MATS")
		:AddUniqueStringField("itemString")
		:AddNumberField("numNeed")
		:AddNumberField("numHave")
		:AddStringField("sourcesStr")
		:Commit()
	private.queuedCraftsUpdateQuery = TSM.Crafting.CreateQueuedCraftsQuery()
		:SetUpdateCallback(private.OnQueuedCraftsUpdated)
	private.OnQueuedCraftsUpdated()
	BagTracking.RegisterCallback(function()
		Delay.AfterTime("GATHERING_BAG_UPDATE", 1, private.UpdateDB)
	end)
end

function Gathering.SetContextChangedCallback(callback)
	private.contextChangedCallback = callback
end

function Gathering.CreateQuery()
	return private.db:NewQuery()
end

function Gathering.SetCrafter(crafter)
	if crafter == TSM.db.factionrealm.gatheringContext.crafter then
		return
	end
	TSM.db.factionrealm.gatheringContext.crafter = crafter
	wipe(TSM.db.factionrealm.gatheringContext.professions)
	private.UpdateProfessionList()
	private.UpdateDB()
end

function Gathering.SetProfessions(professions)
	local numProfessions = Table.Count(TSM.db.factionrealm.gatheringContext.professions)
	local didChange = false
	if numProfessions ~= #professions then
		didChange = true
	else
		for _, profession in ipairs(professions) do
			if not TSM.db.factionrealm.gatheringContext.professions[profession] then
				didChange = true
			end
		end
	end
	if not didChange then
		return
	end
	wipe(TSM.db.factionrealm.gatheringContext.professions)
	for _, profession in ipairs(professions) do
		assert(private.professionList[profession])
		TSM.db.factionrealm.gatheringContext.professions[profession] = true
	end
	private.UpdateDB()
end

function Gathering.GetCrafterList()
	return private.crafterList
end

function Gathering.GetCrafter()
	return TSM.db.factionrealm.gatheringContext.crafter ~= "" and TSM.db.factionrealm.gatheringContext.crafter or nil
end

function Gathering.GetProfessionList()
	return private.professionList
end

function Gathering.GetProfessions()
	return TSM.db.factionrealm.gatheringContext.professions
end

function Gathering.SourcesStrToTable(sourcesStr, info, alts)
	for source, num, characters in gmatch(sourcesStr, "([a-zA-Z]+)/([0-9]+)/([^,]*)") do
		info[source] = tonumber(num)
		if source == "alt" or source == "altGuildBank" then
			for character in gmatch(characters, "([^`]+)") do
				alts[character] = true
			end
		end
	end
end



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

function private.UpdateCrafterList()
	local query = TSM.Crafting.CreateQueuedCraftsQuery()
		:Select("players")
		:Distinct("players")
	wipe(private.crafterList)
	for _, players in query:Iterator() do
		for character in gmatch(players, "[^,]+") do
			if not private.crafterList[character] then
				private.crafterList[character] = true
				tinsert(private.crafterList, character)
			end
		end
	end
	query:Release()

	if TSM.db.factionrealm.gatheringContext.crafter ~= "" and not private.crafterList[TSM.db.factionrealm.gatheringContext.crafter] then
		-- the crafter which was selected no longer exists, so clear the selection
		TSM.db.factionrealm.gatheringContext.crafter = ""
	elseif #private.crafterList == 1 then
		-- there is only one crafter in the list, so select it
		TSM.db.factionrealm.gatheringContext.crafter = private.crafterList[1]
	end
	if TSM.db.factionrealm.gatheringContext.crafter == "" then
		wipe(TSM.db.factionrealm.gatheringContext.professions)
	end
end

function private.UpdateProfessionList()
	-- update the professionList
	wipe(private.professionList)
	if TSM.db.factionrealm.gatheringContext.crafter ~= "" then
		-- populate the list of professions
		local query = TSM.Crafting.CreateQueuedCraftsQuery()
			:Select("profession")
			:Custom(private.QueryPlayerFilter, TSM.db.factionrealm.gatheringContext.crafter)
			:Distinct("profession")
		for _, profession in query:Iterator() do
			private.professionList[profession] = true
			tinsert(private.professionList, profession)
		end
		query:Release()
	end

	-- remove selected professions which are no longer in the list
	for profession in pairs(TSM.db.factionrealm.gatheringContext.professions) do
		if not private.professionList[profession] then
			TSM.db.factionrealm.gatheringContext.professions[profession] = nil
		end
	end

	-- select all professions by default
	if not next(TSM.db.factionrealm.gatheringContext.professions) then
		for _, profession in ipairs(private.professionList) do
			TSM.db.factionrealm.gatheringContext.professions[profession] = true
		end
	end
end

function private.OnQueuedCraftsUpdated()
	private.UpdateCrafterList()
	private.UpdateProfessionList()
	private.UpdateDB()
	private.contextChangedCallback()
end

function private.UpdateDB()
	-- delay the update if we're in combat
	if InCombatLockdown() then
		Delay.AfterTime("DELAYED_GATHERING_UPDATE", 1, private.UpdateDB)
		return
	end
	local crafter = TSM.db.factionrealm.gatheringContext.crafter
	if crafter == "" or not next(TSM.db.factionrealm.gatheringContext.professions) then
		private.db:Truncate()
		return
	end

	local matsNumNeed = TempTable.Acquire()
	local query = TSM.Crafting.CreateQueuedCraftsQuery()
		:Select("spellId", "num")
		:Custom(private.QueryPlayerFilter, crafter)
		:Or()
	for profession in pairs(TSM.db.factionrealm.gatheringContext.professions) do
		query:Equal("profession", profession)
	end
	query:End()
	for _, spellId, numQueued in query:Iterator() do
		for _, itemString, quantity in TSM.Crafting.MatIterator(spellId) do
			matsNumNeed[itemString] = (matsNumNeed[itemString] or 0) + quantity * numQueued
		end
	end
	query:Release()

	local matQueue = TempTable.Acquire()
	local matsNumHave = TempTable.Acquire()
	local matsNumHaveExtra = TempTable.Acquire()
	for itemString, numNeed in pairs(matsNumNeed) do
		matsNumHave[itemString] = private.GetCrafterInventoryQuantity(itemString)
		local numUsed = nil
		numNeed, numUsed = private.HandleNumHave(itemString, numNeed, matsNumHave[itemString])
		if numUsed < matsNumHave[itemString] then
			matsNumHaveExtra[itemString] = matsNumHave[itemString] - numUsed
		end
		if numNeed > 0 then
			matsNumNeed[itemString] = numNeed
			tinsert(matQueue, itemString)
		else
			matsNumNeed[itemString] = nil
		end
	end

	local sourceList = TempTable.Acquire()
	local matSourceList = TempTable.Acquire()
	while #matQueue > 0 do
		local itemString = tremove(matQueue)
		wipe(sourceList)
		local numNeed = matsNumNeed[itemString]
		-- always add a task to get mail on the crafter if possible
		numNeed = private.ProcessSource(itemString, numNeed, "openMail", sourceList)
		assert(numNeed >= 0)
		for _, source in ipairs(TSM.db.profile.gatheringOptions.sources) do
			local isCraftSource = source == "craftProfit" or source == "craftNoProfit"
			local ignoreSource = false
			if isCraftSource then
				-- check if we are already crafting some materials of this craft so shouldn't craft this item
				local spellId = TSM.Crafting.GetMostProfitableSpellIdByItem(itemString, crafter, true)
				if spellId then
					for _, matItemString in TSM.Crafting.MatIterator(spellId) do
						if not ignoreSource and matSourceList[matItemString] and strmatch(matSourceList[matItemString], "craft[a-zA-Z]+/[^,]+/") then
							ignoreSource = true
						end
					end
				else
					-- can't craft this item
					ignoreSource = true
				end
			end
			if not ignoreSource then
				local prevNumNeed = numNeed
				numNeed = private.ProcessSource(itemString, numNeed, source, sourceList)
				assert(numNeed >= 0)
				if numNeed == 0 then
					if isCraftSource then
						-- we are crafting these, so add the necessary mats
						local spellId = TSM.Crafting.GetMostProfitableSpellIdByItem(itemString, crafter, true)
						assert(spellId)
						local numToCraft = ceil(prevNumNeed / TSM.Crafting.GetNumResult(spellId))
						for _, intMatItemString, intMatQuantity in TSM.Crafting.MatIterator(spellId) do
							local intMatNumNeed, numUsed = private.HandleNumHave(intMatItemString, numToCraft * intMatQuantity, matsNumHaveExtra[intMatItemString] or 0)
							if numUsed > 0 then
								matsNumHaveExtra[intMatItemString] = matsNumHaveExtra[intMatItemString] - numUsed
							end
							if intMatNumNeed > 0 then
								if not matsNumNeed[intMatItemString] then
									local intMatNumHave = private.GetCrafterInventoryQuantity(intMatItemString)
									if intMatNumNeed > intMatNumHave then
										matsNumHave[intMatItemString] = intMatNumHave
										matsNumNeed[intMatItemString] = intMatNumNeed - intMatNumHave
										tinsert(matQueue, intMatItemString)
									elseif intMatNumHave > intMatNumNeed then
										matsNumHaveExtra[intMatItemString] = intMatNumHave - intMatNumNeed
									end
								else
									matsNumNeed[intMatItemString] = (matsNumNeed[intMatItemString] or 0) + intMatNumNeed
									if matSourceList[intMatItemString] then
										-- already processed this item, so queue it again
										tinsert(matQueue, intMatItemString)
									end
								end
							end
						end
					end
					break
				end
			end
		end
		sort(sourceList)
		matSourceList[itemString] = table.concat(sourceList, ",")
	end
	private.db:TruncateAndBulkInsertStart()
	for itemString, numNeed in pairs(matsNumNeed) do
		private.db:BulkInsertNewRow(itemString, numNeed, matsNumHave[itemString], matSourceList[itemString])
	end
	private.db:BulkInsertEnd()

	TempTable.Release(sourceList)
	TempTable.Release(matSourceList)
	TempTable.Release(matsNumNeed)
	TempTable.Release(matsNumHave)
	TempTable.Release(matsNumHaveExtra)
	TempTable.Release(matQueue)
end

function private.ProcessSource(itemString, numNeed, source, sourceList)
	local crafter = TSM.db.factionrealm.gatheringContext.crafter
	local playerName = UnitName("player")
	if source == "openMail" then
		local crafterMailQuantity = Inventory.GetMailQuantity(itemString, crafter)
		if crafterMailQuantity > 0 then
			crafterMailQuantity = min(crafterMailQuantity, numNeed)
			if crafter == playerName then
				tinsert(sourceList, "openMail/"..crafterMailQuantity.."/")
			else
				tinsert(sourceList, "alt/"..crafterMailQuantity.."/"..crafter)
			end
			return numNeed - crafterMailQuantity
		end
	elseif source == "vendor" then
		if ItemInfo.GetVendorBuy(itemString) then
			-- assume we can buy all we need from the vendor
			tinsert(sourceList, "vendor/"..numNeed.."/")
			return 0
		end
	elseif source == "guildBank" then
		local guild = PlayerInfo.GetPlayerGuild(crafter)
		local guildBankQuantity = guild and Inventory.GetGuildQuantity(itemString, guild) or 0
		if guildBankQuantity > 0 then
			guildBankQuantity = min(guildBankQuantity, numNeed)
			if crafter == playerName then
				-- we are on the crafter
				tinsert(sourceList, "guildBank/"..guildBankQuantity.."/")
			else
				-- need to switch to the crafter to get items from the guild bank
				tinsert(sourceList, "altGuildBank/"..guildBankQuantity.."/"..crafter)
			end
			return numNeed - guildBankQuantity
		end
	elseif source == "alt" then
		if ItemInfo.IsSoulbound(itemString) then
			-- can't mail soulbound items
			return numNeed
		end
		if crafter ~= playerName then
			-- we are on the alt, so see if we can gather items from this character
			local bagQuantity = Inventory.GetBagQuantity(itemString)
			local bankQuantity = Inventory.GetBankQuantity(itemString) + Inventory.GetReagentBankQuantity(itemString)
			local mailQuantity = Inventory.GetMailQuantity(itemString)

			if bagQuantity > 0 then
				bagQuantity = min(numNeed, bagQuantity)
				tinsert(sourceList, "sendMail/"..bagQuantity.."/")
				numNeed = numNeed - bagQuantity
				if numNeed == 0 then
					return 0
				end
			end
			if mailQuantity > 0 then
				mailQuantity = min(numNeed, mailQuantity)
				tinsert(sourceList, "openMail/"..mailQuantity.."/")
				numNeed = numNeed - mailQuantity
				if numNeed == 0 then
					return 0
				end
			end
			if bankQuantity > 0 then
				bankQuantity = min(numNeed, bankQuantity)
				tinsert(sourceList, "bank/"..bankQuantity.."/")
				numNeed = numNeed - bankQuantity
				if numNeed == 0 then
					return 0
				end
			end
		end

		-- check alts
		local altNum = 0
		local altCharacters = TempTable.Acquire()
		for factionrealm in TSM.db:GetConnectedRealmIterator("factionrealm") do
			for _, character in TSM.db:FactionrealmCharacterIterator(factionrealm) do
				local characterKey = nil
				if factionrealm == UnitFactionGroup("player").." - "..GetRealmName() then
					characterKey = character
				else
					characterKey = character.." - "..factionrealm
				end
				if characterKey ~= crafter and characterKey ~= playerName then
					local num = 0
					num = num + Inventory.GetBagQuantity(itemString, character, factionrealm)
					num = num + Inventory.GetBankQuantity(itemString, character, factionrealm)
					num = num + Inventory.GetReagentBankQuantity(itemString, character, factionrealm)
					num = num + Inventory.GetMailQuantity(itemString, character, factionrealm)
					if num > 0 then
						tinsert(altCharacters, characterKey)
						altNum = altNum + num
					end
				end
			end
		end

		local altCharactersStr = table.concat(altCharacters, "`")
		TempTable.Release(altCharacters)
		if altNum > 0 then
			altNum = min(altNum, numNeed)
			tinsert(sourceList, "alt/"..altNum.."/"..altCharactersStr)
			return numNeed - altNum
		end
	elseif source == "altGuildBank" then
		local currentGuild = PlayerInfo.GetPlayerGuild(playerName)
		if currentGuild and crafter ~= playerName then
			-- we are on an alt, so see if we can gather items from this character's guild bank
			local guildBankQuantity = Inventory.GetGuildQuantity(itemString)
			if guildBankQuantity > 0 then
				guildBankQuantity = min(numNeed, guildBankQuantity)
				tinsert(sourceList, "guildBank/"..guildBankQuantity.."/")
				numNeed = numNeed - guildBankQuantity
				if numNeed == 0 then
					return 0
				end
			end
		end

		-- check alts
		local totalGuildBankQuantity = 0
		local altCharacters = TempTable.Acquire()
		for _, character in PlayerInfo.CharacterIterator(true) do
			local guild = PlayerInfo.GetPlayerGuild(character)
			if guild and guild ~= currentGuild then
				local guildBankQuantity = Inventory.GetGuildQuantity(itemString, guild)
				if guildBankQuantity > 0 then
					tinsert(altCharacters, character)
					totalGuildBankQuantity = totalGuildBankQuantity + guildBankQuantity
				end
			end
		end
		local altCharactersStr = table.concat(altCharacters, "`")
		TempTable.Release(altCharacters)
		if totalGuildBankQuantity > 0 then
			totalGuildBankQuantity = min(totalGuildBankQuantity, numNeed)
			tinsert(sourceList, "altGuildBank/"..totalGuildBankQuantity.."/"..altCharactersStr)
			return numNeed - totalGuildBankQuantity
		end
	elseif source == "craftProfit" or source == "craftNoProfit" then
		local spellId, maxProfit = TSM.Crafting.GetMostProfitableSpellIdByItem(itemString, crafter, true)
		if spellId and (source == "craftNoProfit" or (maxProfit and maxProfit > 0)) then
			-- assume we can craft all we need
			local numToCraft = ceil(numNeed / TSM.Crafting.GetNumResult(spellId))
			tinsert(sourceList, source.."/"..numToCraft.."/")
			return 0
		end
	elseif source == "auction" then
		if ItemInfo.IsSoulbound(itemString) then
			-- can't buy soulbound items
			return numNeed
		end
		-- assume we can buy all we need from the AH
		tinsert(sourceList, "auction/"..numNeed.."/")
		return 0
	elseif source == "auctionCrafting" then
		if ItemInfo.IsSoulbound(itemString) then
			-- can't buy soulbound items
			return numNeed
		end
		if not Conversions.GetSourceItems(itemString) then
			-- can't convert to get this item
			return numNeed
		end
		-- assume we can buy all we need from the AH
		tinsert(sourceList, "auctionCrafting/"..numNeed.."/")
		return 0
	elseif source == "auctionDE" then
		if ItemInfo.IsSoulbound(itemString) then
			-- can't buy soulbound items
			return numNeed
		end
		if not DisenchantInfo.IsTargetItem(itemString) then
			-- can't disenchant to get this item
			return numNeed
		end
		-- assume we can buy all we need from the AH
		tinsert(sourceList, "auctionDE/"..numNeed.."/")
		return 0
	else
		error("Unkown source: "..tostring(source))
	end
	return numNeed
end

function private.QueryPlayerFilter(row, player)
	return String.SeparatedContains(row:GetField("players"), ",", player)
end

function private.GetCrafterInventoryQuantity(itemString)
	local crafter = TSM.db.factionrealm.gatheringContext.crafter
	return Inventory.GetBagQuantity(itemString, crafter) + Inventory.GetReagentBankQuantity(itemString, crafter) + Inventory.GetBankQuantity(itemString, crafter)
end

function private.HandleNumHave(itemString, numNeed, numHave)
	if numNeed > numHave then
		-- use everything we have
		numNeed = numNeed - numHave
		return numNeed, numHave
	else
		-- we have at least as many as we need, so use all of them
		return 0, numNeed
	end
end
