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

local _, TSM = ...
local Banking = TSM:NewPackage("Banking")
local Event = TSM.Include("Util.Event")
local TempTable = TSM.Include("Util.TempTable")
local String = TSM.Include("Util.String")
local Log = TSM.Include("Util.Log")
local ItemString = TSM.Include("Util.ItemString")
local Threading = TSM.Include("Service.Threading")
local ItemInfo = TSM.Include("Service.ItemInfo")
local private = {
	moveThread = nil,
	moveItems = {},
	restoreItems = {},
	restoreFrame = nil,
	callback = nil,
	openFrame = nil,
	frameCallbacks = {},
}
local MOVE_WAIT_TIMEOUT = 2



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

function Banking.OnInitialize()
	private.moveThread = Threading.New("BANKING_MOVE", private.MoveThread)

	Event.Register("BANKFRAME_OPENED", private.BankOpened)
	Event.Register("BANKFRAME_CLOSED", private.BankClosed)
	if not TSM.IsWowClassic() then
		Event.Register("GUILDBANKFRAME_OPENED", private.GuildBankOpened)
		Event.Register("GUILDBANKFRAME_CLOSED", private.GuildBankClosed)
	end
end

function Banking.RegisterFrameCallback(callback)
	tinsert(private.frameCallbacks, callback)
end

function Banking.IsGuildBankOpen()
	return private.openFrame == "GUILD_BANK"
end

function Banking.IsBankOpen()
	return private.openFrame == "BANK"
end

function Banking.MoveToBag(items, callback)
	assert(private.openFrame)
	local context = Banking.IsGuildBankOpen() and Banking.MoveContext.GetGuildBankToBag() or Banking.MoveContext.GetBankToBag()
	private.StartMove(items, context, callback)
end

function Banking.MoveToBank(items, callback)
	assert(private.openFrame)
	local context = Banking.IsGuildBankOpen() and Banking.MoveContext.GetBagToGuildBank() or Banking.MoveContext.GetBagToBank()
	private.StartMove(items, context, callback)
end

function Banking.EmptyBags(callback)
	assert(private.openFrame)
	local items = TempTable.Acquire()
	for _, _, _, itemString, quantity in Banking.Util.BagIterator(false) do
		items[itemString] = (items[itemString] or 0) + quantity
	end
	wipe(private.restoreItems)
	private.restoreFrame = private.openFrame
	private.callback = callback
	local context = Banking.IsGuildBankOpen() and Banking.MoveContext.GetBagToGuildBank() or Banking.MoveContext.GetBagToBank()
	private.StartMove(items, context, private.EmptyBagsThreadCallbackWrapper)
	TempTable.Release(items)
end

function Banking.RestoreBags(callback)
	assert(private.openFrame)
	assert(Banking.CanRestoreBags())
	private.callback = callback
	local context = Banking.IsGuildBankOpen() and Banking.MoveContext.GetGuildBankToBag() or Banking.MoveContext.GetBankToBag()
	private.StartMove(private.restoreItems, context, private.RestoreBagsThreadCallbackWrapper)
end

function Banking.CanRestoreBags()
	assert(private.openFrame)
	return private.openFrame == private.restoreFrame
end

function Banking.PutByFilter(filterStr)
	if not private.openFrame then
		return
	end
	local filterItemString = ItemString.Get(filterStr)
	filterStr = String.Escape(strlower(filterStr))

	local items = TempTable.Acquire()
	for _, _, _, itemString, quantity in Banking.Util.BagIterator(false) do
		items[itemString] = (items[itemString] or 0) + quantity
	end

	for itemString in pairs(items) do
		if not private.MatchesFilter(itemString, filterStr, filterItemString) then
			-- remove this item
			items[itemString] = nil
		end
	end

	Banking.MoveToBank(items, private.GetPutCallback)
	TempTable.Release(items)
end

function Banking.GetByFilter(filterStr)
	if not private.openFrame then
		return
	end
	local filterItemString = ItemString.Get(filterStr)
	filterStr = String.Escape(strlower(filterStr))

	local items = TempTable.Acquire()
	for _, _, _, itemString, quantity in Banking.Util.OpenBankIterator(false) do
		items[itemString] = (items[itemString] or 0) + quantity
	end

	for itemString in pairs(items) do
		if not private.MatchesFilter(itemString, filterStr, filterItemString) then
			-- remove this item
			items[itemString] = nil
		end
	end

	Banking.MoveToBag(items, private.GetPutCallback)
	TempTable.Release(items)
end



-- ============================================================================
-- Threads
-- ============================================================================

function private.MoveThread(context, callback)
	local numMoves = 0
	local emptySlotIds = Threading.AcquireSafeTempTable()
	context:GetEmptySlotsThreaded(emptySlotIds)
	local slotIds = Threading.AcquireSafeTempTable()
	local slotItemString = Threading.AcquireSafeTempTable()
	local slotMoveQuantity = Threading.AcquireSafeTempTable()
	local slotEndQuantity = Threading.AcquireSafeTempTable()
	for itemString, numQueued in pairs(private.moveItems) do
		for _, slotId, quantity in context:SlotIdIterator(itemString) do
			if numQueued > 0 then
				-- find a suitable empty slot
				local targetSlotId = context:GetTargetSlotId(itemString, emptySlotIds)
				if targetSlotId then
					assert(not slotIds[slotId])
					slotIds[slotId] = targetSlotId
					slotItemString[slotId] = itemString
					slotMoveQuantity[slotId] = min(quantity, numQueued)
					slotEndQuantity[slotId] = max(quantity - numQueued, 0)
					numQueued = numQueued - slotMoveQuantity[slotId]
					numMoves = numMoves + 1
				else
					Log.Err("No target slot")
				end
			end
		end
		if numQueued > 0 then
			Log.Err("No slots with item (%s)", itemString)
		end
	end

	local numDone = 0
	while next(slotIds) do
		local movedSlotId = nil
		-- do all the pending moves
		for slotId, targetSlotId in pairs(slotIds) do
			context:MoveSlot(slotId, targetSlotId, slotMoveQuantity[slotId])
			Threading.Yield()
			if private.openFrame == "GUILD_BANK" then
				movedSlotId = slotId
				break
			end
		end

		-- wait for at least one to finish or the timeout to elapse
		local didMove = false
		local timeout = GetTime() + MOVE_WAIT_TIMEOUT
		while not didMove and GetTime() < timeout do
			-- check which moves are done
			for slotId in pairs(slotIds) do
				if private.openFrame ~= "GUILD_BANK" or slotId == movedSlotId then
					if context:GetSlotQuantity(slotId) <= slotEndQuantity[slotId] then
						didMove = true
						slotIds[slotId] = nil
						numDone = numDone + 1
						callback("MOVED", slotItemString[slotId], slotMoveQuantity[slotId])
					end
					if didMove and slotId == movedSlotId then
						break
					end
					Threading.Yield()
				end
			end
			if didMove then
				callback("PROGRESS", numDone / numMoves)
			end
			Threading.Yield(true)
		end
	end

	if private.openFrame == "GUILD_BANK" then
		QueryGuildBankTab(GetCurrentGuildBankTab())
	end

	Threading.ReleaseSafeTempTable(slotIds)
	Threading.ReleaseSafeTempTable(slotItemString)
	Threading.ReleaseSafeTempTable(slotMoveQuantity)
	Threading.ReleaseSafeTempTable(slotEndQuantity)
	Threading.ReleaseSafeTempTable(emptySlotIds)
	callback("DONE")
end



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

function private.BankOpened()
	if private.openFrame == "BANK" then
		return
	end
	assert(not private.openFrame)
	private.openFrame = "BANK"
	for _, callback in ipairs(private.frameCallbacks) do
		callback(private.openFrame)
	end
end

function private.GuildBankOpened()
	if private.openFrame == "GUILD_BANK" then
		return
	end
	assert(not private.openFrame)
	private.openFrame = "GUILD_BANK"
	for _, callback in ipairs(private.frameCallbacks) do
		callback(private.openFrame)
	end
end

function private.BankClosed()
	if not private.openFrame then
		return
	end
	private.openFrame = nil
	private.StopMove()
	for _, callback in ipairs(private.frameCallbacks) do
		callback(private.openFrame)
	end
end

function private.GuildBankClosed()
	if not private.openFrame then
		return
	end
	private.openFrame = nil
	private.StopMove()
	for _, callback in ipairs(private.frameCallbacks) do
		callback(private.openFrame)
	end
end

function private.StartMove(items, context, callback)
	private.StopMove()
	wipe(private.moveItems)
	for itemString, quantity in pairs(items) do
		private.moveItems[itemString] = quantity
	end
	Threading.Start(private.moveThread, context, callback)
end

function private.StopMove()
	Threading.Kill(private.moveThread)
end

function private.EmptyBagsThreadCallbackWrapper(event, ...)
	if event == "MOVED" then
		local itemString, numMoved = ...
		private.restoreItems[itemString] = (private.restoreItems[itemString] or 0) + numMoved
	elseif event == "DONE" then
		if not next(private.restoreItems) then
			private.restoreFrame = private.openFrame
		end
	end
	private.callback(event, ...)
end

function private.RestoreBagsThreadCallbackWrapper(event, ...)
	if event == "DONE" then
		wipe(private.restoreItems)
		private.restoreFrame = nil
	end
	private.callback(event, ...)
end

function private.GetPutCallback(event)
	if event == "DONE" then
		Log.PrintUser(DONE)
	end
end

function private.MatchesFilter(itemString, filterStr, filterItemString)
	local name = strlower(ItemInfo.GetName(itemString) or "")
	return strmatch(ItemString.GetBase(itemString), filterStr) or strmatch(name, filterStr) or (filterItemString and itemString == filterItemString)
end
