local DEBUG =
-- function() end
d

local function _tr(str)
	return str
end

local IM = InventoryManager

local RevCaches = nil
local Empties = nil

local function CreateReverseCache(bagType)
	local _revCache = { }
	local _empties = { }

	for i = 0, GetBagSize(bagType)-1, 1 do
		local curStack, maxStack = GetSlotStackSize(bagType, i)
		if curStack > 0 then
			local id = GetItemInstanceId(bagType, i)
			if curStack < maxStack then
				if not _revCache[id] then _revCache[id] = { } end
				local entry = _revCache[id]
				entry[i] = { curStack, maxStack }
			end
		else
			_empties[#_empties + 1] = i
		end
	end
	return _revCache, _empties
end

local function CreateReverseCaches()
	RevCaches = { }
	Empties = { }
	RevCaches[BAG_BACKPACK], 	Empties[BAG_BACKPACK] 	= CreateReverseCache(BAG_BACKPACK)
	RevCaches[BAG_BANK], 		Empties[BAG_BANK] 		= CreateReverseCache(BAG_BANK)
end

local function UpdateCaches(srcBagType, srcSlotId, tgtBagType, tgtSlotId, count)
	local id = GetItemInstanceId(srcBagType, srcSlotId)
	local stack

	stack = RevCaches[srcBagType][id] and RevCaches[srcBagType][id][srcSlotId]
	if stack then
		stack[1] = stack[1] - count
		if stack[1] == 0 then
			local newempties = Empties[srcBagType]
			newempties[#newempties + 1] = srcSlotId
			RevCaches[srcBagType][id][srcSlotId] = nil
		end
	else
		local newempties = Empties[srcBagType]
		newempties[#newempties + 1] = srcSlotId
	end

	stack = RevCaches[tgtBagType][id] and RevCaches[tgtBagType][id][tgtSlotId]
	if stack then
		stack[1] = stack[1] + count
		if stack[1] == stack[2] then
			RevCaches[tgtBagType][id][tgtSlotId] = nil
		end
	else
		local _, maxStack = GetSlotStackSize(srcBagType, srcSlotId)
		if count < maxStack then
			-- We ended up with a new incomplete stack.
			if not RevCaches[tgtBagType][id] then RevCaches[tgtBagType][id] = { } end
			local newstack = RevCaches[tgtBagType][id]
			newstack[tgtSlotId] = { count, maxStack }
		end
	end
end

-- Returns (empties source?), tgtSlotId, transferCount
local function FindTargetSlot(srcBagType, srcSlotId, tgtBagType)
	local curStack, maxStack = GetSlotStackSize(srcBagType, srcSlotId)
	local id = GetItemInstanceId(srcBagType, srcSlotId)

	local stacks = RevCaches[tgtBagType][id]
	if stacks then
		-- First, try to find a stack small enough to hold the entirety of the source
		for tgtSlotId, v in pairs(stacks) do
			if v[2]-v[1] >= curStack then
				UpdateCaches(srcBagType, srcSlotId, tgtBagType, tgtSlotId, curStack)
				return true, tgtSlotId, curStack
			end
		end

		-- Now, fill any incomplete stack we might have, splitting the source stack
		for tgtSlotId, v in pairs(stacks) do
			local missing = v[2] - v[1]
			if missing > 0 then
				UpdateCaches(srcBagType, srcSlotId, tgtBagType, tgtSlotId, missing)
				return false, tgtSlotId, missing
			end
		end
	end

	-- All the stacks we might have found are already full, we need to find a free slot
	local empties = Empties[tgtBagType]

	-- No such luck?
	if #empties == 0 then return false, -1, 0 end

	-- It's a complete move, remove the empty slot from the target list, and create a new one on the source list
	local tgtSlotId = empties[#empties]
	empties[#empties] = nil

	UpdateCaches(srcBagType, srcSlotId, tgtBagType, tgtSlotId, curStack)
	return true, tgtSlotId, curStack

end


local function CollectSingleDirection(action)
	local bagType = (action == InventoryManager.ACTION_STASH and BAG_BACKPACK) or BAG_BANK
	local _moveSlots = { }

	InventoryManager:SetCurrentInventory(bagType)

	for i,_ in pairs(InventoryManager.currentInventory) do
		local data = InventoryManager:GetItemData(i)
		if action == InventoryManager.currentRuleset:Match(data) then
			_moveSlots[#_moveSlots + 1] = i
		end
	end
	return _moveSlots
end

local function CalculateSingleMove(direction)
	local srcBagType = (direction == 1 and BAG_BACKPACK) or BAG_BANK
	local tgtBagType = (direction == 1 and BAG_BANK) or BAG_BACKPACK

	local srcSlotRepo = Moves[(direction == 1 and "stash") or "retrieve"]
	if #srcSlotRepo == 0 then return "src_empty" end

	local srcSlotId = srcSlotRepo[#srcSlotRepo]

	local empties, tgtSlotId, count = FindTargetSlot(srcBagType, srcSlotId, tgtBagType)
	if count == 0 then return "tgt_full" end

	if empties then srcSlotRepo[#srcSlotRepo] = nil end

	return "ok", {
		["srcbag"] = srcBagType,
		["srcslot"] = srcSlotId,
		["tgtbag"] = tgtBagType,
		["tgtslot"] = tgtSlotId,
		["count"] = count
	}
end

local function CalculateMoves()
	-- Prepare an overview of the inventories and the pending transfers in both directions
	CreateReverseCaches()
	local continue = true
	local _moveStack = { }

	InventoryManager.currentRuleset:ResetCounters()

    Moves = {
		["stash"] = CollectSingleDirection(IM.ACTION_STASH),
		["retrieve"] = CollectSingleDirection(IM.ACTION_RETRIEVE)
    }

	-- We alternate between stashing and retrieving to minimize the chance of one
	-- of the inventories running full.
	while continue do
		local leftres, leftentry = CalculateSingleMove(1)
		local rightres, rightentry = CalculateSingleMove(-1)

		-- ZOS Spam limitation
		if #_moveStack > 95 then
			return "limited", _moveStack
		end

		-- Both inventories full, can't move anything
		if leftres == "tgt_full" and rightres == "tgt_full" then
			return "deadlock", _moveStack
		end

		-- We completed all we were set out to do
		if leftres == "src_empty" and rightres == "src_empty" then
			return "ok", _moveStack
		end

		-- We filled up one side, but we can't empty it out
		if leftres ~= "ok" and rightres ~= "ok" then
			return "partial", _moveStack
		end

		if leftres == "ok" then
			_moveStack[#_moveStack + 1] = leftentry
		end

		if rightres == "ok" then
			_moveStack[#_moveStack + 1] = rightentry
		end
	end
	-- NOTREACHED
end

InventoryManager.moveStatus = nil

function ProcessMove(move)
	local bagIdFrom = move["srcbag"]
	local slotIdFrom = move["srcslot"]
	local bagIdTo = move["tgtbag"]
	local slotIdTo = move["tgtslot"]
	local qtyToMove = move["count"]
	local action = (bagIdFrom == BAG_BACKPACK and InventoryManager.ACTION_STASH) or InventoryManager.ACTION_RETRIEVE

	local data = InventoryManager:GetItemData(slotIdFrom, SHARED_INVENTORY:GetOrCreateBagCache(bagIdFrom))
	InventoryManager:ReportAction(data, false, action)

	if IsProtectedFunction("RequestMoveItem") then
		CallSecureProtected("RequestMoveItem", bagIdFrom, slotIdFrom, bagIdTo, slotIdTo, qtyToMove)
	else
		RequestMoveItem(bagIdFrom, slotIdFrom, bagIdTo, slotIdTo, qtyToMove)
	end
end

function InventoryManager:FinishMoves()
	local result
	if self.moveStatus == "limited" then
		result = GetString(IM_BANK_LIMITED)
	elseif self.moveStatus == "deadlock" then
		result = GetString(IM_BANK_DEADLOCK)
	elseif self.moveStatus == "partial" then
		result = GetString(IM_BANK_PARTIAL)
	elseif self.moveStatus == "ok" then
		result = GetString(IM_BANK_OK)
	end

	if result ~= "" then
		CHAT_SYSTEM:AddMessage(result)
	end
end

function InventoryManager:BalanceCurrency(currencyType, minCur, maxCur, curName)
	local carried = GetCarriedCurrencyAmount(currencyType)
	local banked = GetBankedCurrencyAmount(currencyType)

	local move = 0
	if(carried < minCur) then
		move = carried - minCur
	elseif(carried > maxCur) then
		move = carried - maxCur
	end

	if move == 0 then
		return
	elseif move > 0 then
		CHAT_SYSTEM:AddMessage(
			zo_strformat(GetString(IM_CUR_DEPOSIT), move, curName))
		DepositCurrencyIntoBank(currencyType, move)
	else
		move = -move
		if move > banked then move = banked end
		if move == 0 then return end
		CHAT_SYSTEM:AddMessage(
			zo_strformat(GetString(IM_CUR_WITHDRAW), move, curName))
		WithdrawCurrencyFromBank(currencyType, move)
	end
end

local function event_filter_fn(eventCode, bagId, slotId, isNewItem, itemSoundCategory, inventoryUpdateReason, stackCountChange)
	-- Bank moves fire two events, react only if we got the receiver side.
	if not stackCountChange or stackCountChange > 0 then
		return true
	end
	return false
end

function InventoryManager:OnBankOpened()
	local moves
	self.moveStatus, moves = CalculateMoves()

	-- Flip the list. The processors start from the end, and the sequence is important here.
	for i = 1, #moves / 2, 1 do
		local tmp = moves[i]
		moves[i] = moves[(#moves+1) - i]
		moves[(#moves+1) - i] = tmp
	end

	self:BalanceCurrency(CURT_MONEY, self.settings.minGold, self.settings.maxGold, GetString(IM_CUR_GOLD))
	self:BalanceCurrency(CURT_TELVAR_STONES, self.settings.minTV, self.settings.maxTV, GetString(IM_CUR_TVSTONES))

	zo_callLater(
		function()
			IM:DoEventProcessing(moves,
				ProcessMove,
				function() IM:FinishMoves() end,
				EVENT_INVENTORY_SINGLE_SLOT_UPDATE,
				EVENT_CLOSE_BANK,
				InventoryManager.settings.bankMoveDelay,
				event_filter_fn)
		end,
		InventoryManager.settings.bankInitDelay)
end

EVENT_MANAGER:RegisterForEvent(InventoryManager.name, EVENT_OPEN_BANK, function() InventoryManager:OnBankOpened() end)