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

local _, TSM = ...
local MoveContext = TSM.Banking:NewPackage("MoveContext")
local Table = TSM.Include("Util.Table")
local SlotId = TSM.Include("Util.SlotId")
local Threading = TSM.Include("Service.Threading")
local ItemInfo = TSM.Include("Service.ItemInfo")
local InventoryInfo = TSM.Include("Service.InventoryInfo")
local BagTracking = TSM.Include("Service.BagTracking")
local GuildTracking = TSM.Include("Service.GuildTracking")
local private = {
	bagToBank = nil,
	bankToBag = nil,
	bagToGuildBank = nil,
	guildBankToBag = nil,
}
-- don't use MAX_GUILDBANK_SLOTS_PER_TAB since it isn't available right away
local GUILD_BANK_TAB_SLOTS = 98



-- ============================================================================
-- BaseMoveContext Class
-- ============================================================================

local BaseMoveContext = TSM.Include("LibTSMClass").DefineClass("BaseMoveContext", nil, "ABSTRACT")



-- ============================================================================
-- BagToBankMoveContext Class
-- ============================================================================

local BagToBankMoveContext = TSM.Include("LibTSMClass").DefineClass("BagToBankMoveContext", BaseMoveContext)

function BagToBankMoveContext.MoveSlot(self, fromSlotId, toSlotId, quantity)
	local fromBag, fromSlot = SlotId.Split(fromSlotId)
	SplitContainerItem(fromBag, fromSlot, quantity)
	if GetCursorInfo() == "item" then
		PickupContainerItem(SlotId.Split(toSlotId))
	end
	ClearCursor()
end

function BagToBankMoveContext.GetSlotQuantity(self, slotId)
	return private.BagBankGetSlotQuantity(slotId)
end

function BagToBankMoveContext.SlotIdIterator(self, itemString)
	return private.BagSlotIdIterator(itemString)
end

function BagToBankMoveContext.GetEmptySlotsThreaded(self, emptySlotIds)
	local sortValue = Threading.AcquireSafeTempTable()
	if not TSM.IsWowClassic() then
		private.GetEmptySlotsHelper(REAGENTBANK_CONTAINER, emptySlotIds, sortValue)
	end
	private.GetEmptySlotsHelper(BANK_CONTAINER, emptySlotIds, sortValue)
	for bag = NUM_BAG_SLOTS + 1, NUM_BAG_SLOTS + NUM_BANKBAGSLOTS do
		private.GetEmptySlotsHelper(bag, emptySlotIds, sortValue)
	end
	Table.SortWithValueLookup(emptySlotIds, sortValue)
	Threading.ReleaseSafeTempTable(sortValue)
end

function BagToBankMoveContext.GetTargetSlotId(self, itemString, emptySlotIds)
	return private.BagBankGetTargetSlotId(itemString, emptySlotIds)
end



-- ============================================================================
-- BankToBagMoveContext Class
-- ============================================================================

local BankToBagMoveContext = TSM.Include("LibTSMClass").DefineClass("BankToBagMoveContext", BaseMoveContext)

function BankToBagMoveContext.MoveSlot(self, fromSlotId, toSlotId, quantity)
	local fromBag, fromSlot = SlotId.Split(fromSlotId)
	SplitContainerItem(fromBag, fromSlot, quantity)
	if GetCursorInfo() == "item" then
		PickupContainerItem(SlotId.Split(toSlotId))
	end
	ClearCursor()
end

function BankToBagMoveContext.GetSlotQuantity(self, slotId)
	return private.BagBankGetSlotQuantity(slotId)
end

function BankToBagMoveContext.SlotIdIterator(self, itemString)
	itemString = TSM.Groups.TranslateItemString(itemString)
	return BagTracking.CreateQueryBankItem(itemString)
		:VirtualField("autoBaseItemString", "string", TSM.Groups.TranslateItemString, "itemString")
		:Equal("autoBaseItemString", itemString)
		:Select("slotId", "quantity")
		:IteratorAndRelease()
end

function BankToBagMoveContext.GetEmptySlotsThreaded(self, emptySlotIds)
	private.BagGetEmptySlotsThreaded(emptySlotIds)
end

function BankToBagMoveContext.GetTargetSlotId(self, itemString, emptySlotIds)
	return private.BagBankGetTargetSlotId(itemString, emptySlotIds)
end



-- ============================================================================
-- BagToGuildBankMoveContext Class
-- ============================================================================

local BagToGuildBankMoveContext = TSM.Include("LibTSMClass").DefineClass("BagToGuildBankMoveContext", BaseMoveContext)

function BagToGuildBankMoveContext.MoveSlot(self, fromSlotId, toSlotId, quantity)
	local fromBag, fromSlot = SlotId.Split(fromSlotId)
	SplitContainerItem(fromBag, fromSlot, quantity)
	if GetCursorInfo() == "item" then
		PickupGuildBankItem(SlotId.Split(toSlotId))
	end
	ClearCursor()
end

function BagToGuildBankMoveContext.GetSlotQuantity(self, slotId)
	return private.BagBankGetSlotQuantity(slotId)
end

function BagToGuildBankMoveContext.SlotIdIterator(self, itemString)
	return private.BagSlotIdIterator(itemString)
end

function BagToGuildBankMoveContext.GetEmptySlotsThreaded(self, emptySlotIds)
	local currentTab = GetCurrentGuildBankTab()
	local _, _, _, _, numWithdrawals = GetGuildBankTabInfo(currentTab)
	if numWithdrawals == -1 or numWithdrawals >= GUILD_BANK_TAB_SLOTS then
		for slot = 1, GUILD_BANK_TAB_SLOTS do
			if not GetGuildBankItemInfo(currentTab, slot) then
				tinsert(emptySlotIds, SlotId.Join(currentTab, slot))
			end
		end
	end
	for tab = 1, GetNumGuildBankTabs() do
		if tab ~= currentTab then
			-- only use tabs which we have at least enough withdrawals to withdraw every slot
			_, _, _, _, numWithdrawals = GetGuildBankTabInfo(tab)
			if numWithdrawals == -1 or numWithdrawals >= GUILD_BANK_TAB_SLOTS then
				for slot = 1, GUILD_BANK_TAB_SLOTS do
					if not GetGuildBankItemInfo(tab, slot) then
						tinsert(emptySlotIds, SlotId.Join(tab, slot))
					end
				end
			end
		end
	end
end

function BagToGuildBankMoveContext.GetTargetSlotId(self, itemString, emptySlotIds)
	return tremove(emptySlotIds, 1)
end



-- ============================================================================
-- GuildBankToBagMoveContext Class
-- ============================================================================

local GuildBankToBagMoveContext = TSM.Include("LibTSMClass").DefineClass("GuildBankToBagMoveContext", BaseMoveContext)

function GuildBankToBagMoveContext.MoveSlot(self, fromSlotId, toSlotId, quantity)
	local fromTab, fromSlot = SlotId.Split(fromSlotId)
	SplitGuildBankItem(fromTab, fromSlot, quantity)
	if GetCursorInfo() == "item" then
		PickupContainerItem(SlotId.Split(toSlotId))
	end
	ClearCursor()
end

function GuildBankToBagMoveContext.GetSlotQuantity(self, slotId)
	local tab, slot = SlotId.Split(slotId)
	QueryGuildBankTab(tab)
	local _, quantity = GetGuildBankItemInfo(tab, slot)
	return quantity or 0
end

function GuildBankToBagMoveContext.SlotIdIterator(self, itemString)
	itemString = TSM.Groups.TranslateItemString(itemString)
	return GuildTracking.CreateQueryItem(itemString)
		:VirtualField("autoBaseItemString", "string", TSM.Groups.TranslateItemString, "itemString")
		:Equal("autoBaseItemString", itemString)
		:Select("slotId", "quantity")
		:IteratorAndRelease()
end

function GuildBankToBagMoveContext.GetEmptySlotsThreaded(self, emptySlotIds)
	private.BagGetEmptySlotsThreaded(emptySlotIds)
end

function GuildBankToBagMoveContext.GetTargetSlotId(self, itemString, emptySlotIds)
	return private.BagBankGetTargetSlotId(itemString, emptySlotIds)
end



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

function MoveContext.GetBagToBank()
	private.bagToBank = private.bagToBank or BagToBankMoveContext()
	return private.bagToBank
end

function MoveContext.GetBankToBag()
	private.bankToBag = private.bankToBag or BankToBagMoveContext()
	return private.bankToBag
end

function MoveContext.GetBagToGuildBank()
	private.bagToGuildBank = private.bagToGuildBank or BagToGuildBankMoveContext()
	return private.bagToGuildBank
end

function MoveContext.GetGuildBankToBag()
	private.guildBankToBag = private.guildBankToBag or GuildBankToBagMoveContext()
	return private.guildBankToBag
end



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

function private.BagBankGetSlotQuantity(slotId)
	local _, quantity = GetContainerItemInfo(SlotId.Split(slotId))
	return quantity or 0
end

function private.BagSlotIdIterator(itemString)
	itemString = TSM.Groups.TranslateItemString(itemString)
	local query = BagTracking.CreateQueryBagsItem(itemString)
		:Select("slotId", "quantity")
		:VirtualField("autoBaseItemString", "string", TSM.Groups.TranslateItemString, "itemString")
		:Equal("autoBaseItemString", itemString)
	if TSM.Banking.IsGuildBankOpen() then
		query:Equal("isBoA", false)
		query:Equal("isBoP", false)
	end
	return query:IteratorAndRelease()
end

function private.BagGetEmptySlotsThreaded(emptySlotIds)
	local sortValue = Threading.AcquireSafeTempTable()
	for bag = BACKPACK_CONTAINER, NUM_BAG_SLOTS do
		private.GetEmptySlotsHelper(bag, emptySlotIds, sortValue)
	end
	Table.SortWithValueLookup(emptySlotIds, sortValue)
	Threading.ReleaseSafeTempTable(sortValue)
end

function private.GetEmptySlotsHelper(bag, emptySlotIds, sortValue)
	local isSpecial = nil
	if bag == REAGENTBANK_CONTAINER then
		isSpecial = true
	elseif bag == BACKPACK_CONTAINER or bag == BANK_CONTAINER then
		isSpecial = false
	else
		isSpecial = (GetItemFamily(GetInventoryItemLink("player", ContainerIDToInventoryID(bag))) or 0) ~= 0
	end
	for slot = 1, GetContainerNumSlots(bag) do
		if not GetContainerItemInfo(bag, slot) then
			local slotId = SlotId.Join(bag, slot)
			tinsert(emptySlotIds, slotId)
			sortValue[slotId] = slotId + (isSpecial and 0 or 100000)
		end
	end
end

function private.BagBankGetTargetSlotId(itemString, emptySlotIds)
	for i, slotId in ipairs(emptySlotIds) do
		local bag = SlotId.Split(slotId)
		if InventoryInfo.ItemWillGoInBag(ItemInfo.GetLink(itemString), bag) then
			return tremove(emptySlotIds, i)
		end
	end
end
