-- luacheck: globals MyModGlobal Character CLIENT
-- luacheck: max line length 420
if not CLIENT then return end

local utils = require("Cyka.utils")
local dump = require("Cyka.dump")

---@class ItemLocation
---@field inventory Barotrauma.ItemInventory
---@field slotIndex number
---@field depth number
---@field maxFits number

-- The resulting item tree is a table where the key is an ID of an item
-- And the value is an object that represents where that item is located
-- In our inventory
-- Special case are empty slots where any item fits
---@param inventory Barotrauma.ItemInventory
---@param itemTree table<string, ItemLocation[]>
---@param depth number
---@return table<string, ItemLocation[]>
local function buildItemTree(inventory, itemTree, depth)
	itemTree = itemTree or {}
	depth = depth or 0
	if not inventory or not inventory.slots then
		-- MyModGlobal.debugPrint(string.format("Inventory is nil or has no slots, returning empty itemTree"))
		return itemTree
	end

	-- One slot can have one item but multiple of it
	-- The number of an item in a slot is #slot.items
	for slotIndex, slot in ipairs(inventory.slots) do
		-- MyModGlobal.debugPrint(string.format("Building item tree for inventory at slot index: %d", slotIndex))
		-- MyModGlobal.debugPrint(string.format("Slot %d has %d items", slotIndex, #slot.items))
		if #slot.items == 0 then
			-- MyModGlobal.debugPrint(string.format("Slot %d is empty, adding to itemTree as 'empty'", slotIndex))
			itemTree['empty'] = itemTree['empty'] or {}
			itemTree['empty'][#itemTree['empty'] + 1] = {
				inventory = inventory,
				slotIndex = slotIndex - 1,
				maxFits = 60,
				depth = depth
			}
			-- MyModGlobal.debugPrint(string.format("Added empty slot to itemTree at index: %d", slotIndex))
		else
			---@type Barotrauma.Item
			local item = slot.items[1]
			local identifier = item.Prefab.Identifier.Value
			-- MyModGlobal.debugPrint(string.format("Found item: %s with identifier: %s", item.Name, identifier))
			itemTree[identifier] = itemTree[identifier] or {}
			-- We DO want even slots with maxFits = 0
			-- Because that indicates that we DO HAVE the item
			-- At all
			-- And based on that we decide to move it
			itemTree[identifier][#itemTree[identifier] + 1] = {
				inventory = inventory,
				slotIndex = slotIndex - 1,
				maxFits = slot.HowManyCanBePut(item.Prefab),
				depth = depth
			}
			-- MyModGlobal.debugPrint(string.format("Added item to itemTree under identifier: %s", identifier))

			local tags = item.Prefab.Tags
			local shouldSuss = false
			for tag in tags do
				if tag.value:find("container") then
					shouldSuss = true
					break
				end
			end

			if shouldSuss then
				-- MyModGlobal.debugPrint(string.format("Searching inside %s for nested containers", item.Name))
				buildItemTree(item.OwnInventory, itemTree, depth + 1)
			end
		end
	end

	-- MyModGlobal.debugPrint("Completed building item tree")
	return itemTree
end

-- We would like to fill larger stacks first
---@param itemTree table<string, ItemLocation[]>
---@return table<string, ItemLocation[]>
local function sortItemTree(itemTree)
	for _, item in pairs(itemTree) do
		table.sort(item, function(a, b)
			---@cast a ItemLocation
			---@cast b ItemLocation
			if a.depth ~= b.depth then
				return a.depth < b.depth
			elseif a.maxFits ~= b.maxFits then
				return a.maxFits > b.maxFits
			else
				return a.slotIndex < b.slotIndex
			end
		end)
	end

	return itemTree
end

---@param item Barotrauma.Item
---@param itemTree table<string, ItemLocation[]>
---@param force boolean
---@return string
local function tryMoveItem(item, itemTree, force)
	-- MyModGlobal.debugPrint(string.format("Attempting to move item: %s", item.Prefab.Identifier.Value))
	force = force or false
	local location = itemTree[item.Prefab.Identifier.Value]
	if not location and not force then
		-- MyModGlobal.debugPrint("No locations for item, not stacking (not forced)")
		return nil, "No locations for item, not stacking (not forced)"
	end
	-- MyModGlobal.debugPrint(string.format("Attempting to move item: %s", item.Prefab.Identifier.Value))
	-- MyModGlobal.DumpTable(location)

	local moved = false
	if location then
		-- First try to move to existing stacks
		for _, itemLocation in ipairs(location) do
			-- We cannot stack items with decreased condition
			local canBePut = itemLocation.inventory.CanBePutInSlot(item.Prefab, itemLocation.slotIndex, item.Condition)
			-- MyModGlobal.debugPrint(string.format("Can be put in slot %d: %s", itemLocation.slotIndex, tostring(canBePut)))

			if itemLocation.maxFits > 0 and canBePut then
				moved = moved or
					itemLocation.inventory.TryPutItem(item, itemLocation.slotIndex, false, true, nil)
				if moved then
					itemLocation.maxFits = itemLocation.inventory.HowManyCanBePut(item.Prefab, itemLocation.slotIndex)
					break
				end
				-- if moved then
				-- 	MyModGlobal.debugPrint(string.format("Moved item to existing stack at slot index: %d", itemLocation .slotIndex))
				-- else
				-- 	MyModGlobal.debugPrint(string.format("Failed to move item to existing stack at slot index: %d", itemLocation .slotIndex))
				-- end
			end
		end
	end

	-- If we can not find an existing stack
	-- Then move to any of the empty slots
	if not moved then
		-- MyModGlobal.debugPrint("No existing stacks found, trying empty slots...")
		if not itemTree['empty'] then
			return "No empty slots found"
		end
		for _, itemLocation in ipairs(itemTree['empty']) do
			local maxFits = itemLocation.maxFits
			-- These empty slots are not guranteed to be empty, ironically
			-- After we insert an item into one it's no longer empty
			-- But it still is in the empty table
			-- So we want to make sure we can insert our item
			-- Into the maybe empty slots
			itemLocation.maxFits = itemLocation.inventory.HowManyCanBePut(item.Prefab, itemLocation.slotIndex)

			if maxFits > 0 then
				-- MyModGlobal.debugPrint(string.format("Trying to move item to empty slot at index: %d", itemLocation.slotIndex))
				moved = moved or
					itemLocation.inventory.TryPutItem(item, itemLocation.slotIndex, true, false, nil)
				if moved then
					itemLocation.maxFits = itemLocation.inventory.HowManyCanBePut(item.Prefab, itemLocation.slotIndex)
					break
				end
				-- if moved then
				-- 	MyModGlobal.debugPrint(string.format("Moved item to empty slot at index: %d", itemLocation.slotIndex))
				-- else
				-- 	MyModGlobal.debugPrint(string.format("Failed to move item to empty slot at index: %d", itemLocation.slotIndex))
				-- end
			end
		end
	end

	-- If we still can not move the item give up
	if not moved then
		-- MyModGlobal.debugPrint("Failed to find valid location for item")
		return "Failed to find valid location for item"
	end
	-- MyModGlobal.debugPrint("Item moved successfully")
	return nil
end

---@param items Barotrauma.Item[]
---@param itemTree table<string, ItemLocation[]>
---@param force boolean
---@return string[]
local function tryMoveItems(items, itemTree, force)
	force = force or false
	local errs = {}
	for _, item in ipairs(items) do
		local err = tryMoveItem(item, itemTree, force)
		-- oops, this one failed, continue...
		if err then
			errs[#errs + 1] = string.format("Failed to move item: %s", item.Prefab.Identifier.Value)
		end
	end
	return errs
end

---@param character Barotrauma.Character
---@return table<string, ItemLocation[]>, string
local function tryBuildCharacterItemTree(character)
	local itemTree = {}
	-- MyModGlobal.debugPrint(string.format("Preparing to stack items into the bag..."))
	local inventory = character.Inventory
	if not inventory or not inventory.slots then
		return itemTree, "Character has no inventory"
	end

	local bagSlot = inventory.slots[MyModGlobal.BAG_SLOT]
	if bagSlot then
		-- MyModGlobal.debugPrint(string.format("Bag slot found at index 8 with %d items.", #bagSlot.items))
		if #bagSlot.items > 0 then
			local item = bagSlot.items[1]
			-- MyModGlobal.debugPrint(string.format("Found item in bag slot: %s", item.Name))
			if item and item.OwnInventory then
				-- MyModGlobal.debugPrint(string.format("Item has its own inventory, building item tree for it..."))
				itemTree = buildItemTree(item.OwnInventory, itemTree)
			else
				return itemTree, "Bag does not have its own inventory"
			end
		else
			return itemTree, "Bag slot is empty"
		end
	else
		return itemTree, "No bag slot found at index " .. tostring(MyModGlobal.BAG_SLOT)
	end
	return itemTree, nil
end

---@param item Barotrauma.Item
---@return string?
local function stackToContainer(item)
	MyModGlobal.debugPrint(string.format("Attempting to stack items to container: %s", tostring(item)))
	local itemInventory = item.OwnInventory
	if not itemInventory then
		return "Item has no own inventory"
	end
	local parentInventory = item.ParentInventory
	if not parentInventory then
		return "Item has no parent inventory"
	end

	local itemTree = buildItemTree(itemInventory)
	itemTree = sortItemTree(itemTree)

	local toMove = {}
	for slot in parentInventory.slots do
		for slotItem in slot.items do
			if slotItem.Prefab.Identifier.Value ~= item.Prefab.Identifier.Value then
				toMove[#toMove + 1] = slotItem
			end
		end
	end
	MyModGlobal.debugPrint(string.format("Enqueued %d items to stack", #toMove))
	-- dump(toMove)

	local errors = tryMoveItems(toMove, itemTree)
	for _, error in ipairs(errors) do
		MyModGlobal.debugPrint(string.format("Error stacking item: %s", error))
	end
end
-- Function to quickly stack items from inventory to containers
-- 6 and 7 are hands
-- 9..18 are main slots
local inventorySlotsToStack = { 6, 7, }
-- local inventorySlotsToStack = { 6, 7, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18 }
---@param character Barotrauma.Character
local function quickStackItems(character)
	MyModGlobal.debugPrint("Quick stack function called")
	-- If we are mousing over an item that has an inventory (ie. is a container)
	-- Then stack all items from the parent inventory into the mouseover container
	local mouseover = utils.getFirstSlotUnderCursor()
	if mouseover then
		---@type Barotrauma.Item
		local slotItem = mouseover.slot.items[1]
		local itemInventory = slotItem.OwnInventory
		if itemInventory then
			MyModGlobal.debugPrint(string.format("Item inventory found: %s", tostring(itemInventory)))
			local err = stackToContainer(slotItem)
			if err then
				MyModGlobal.debugPrint(string.format("Error stacking items to container: %s", err))
			end
			return
		end
	end

	if not character then
		MyModGlobal.debugPrint("No character found")
		return
	end

	local inventory = character.Inventory
	if not inventory or not inventory.slots then
		MyModGlobal.debugPrint("Character has no inventory")
		return
	end

	local itemTree, err = tryBuildCharacterItemTree(character)
	if err then
		MyModGlobal.debugPrint(string.format("Error building item tree: %s", err))
		return
	end
	itemTree = sortItemTree(itemTree)
	--DumpTable(itemTree)
	local toMove = {}

	for item in character.HeldItems do
		MyModGlobal.debugPrint(string.format("Item: %s", item.Prefab.Identifier.Value))
		if item.OwnInventory then
			toMove = utils.enqueueInventory(item.OwnInventory, toMove)
		end
	end

	-- for _, slotid in ipairs(inventorySlotsToStack) do
	-- 	MyModGlobal.debugPrint(string.format("Processing inventory slot: %d", slotid))
	-- 	local slot = inventory.slots[slotid]
	-- 	if #slot.items > 0 then
	-- 		-- local item = slot.items[1]
	-- 		-- local tags = item.Prefab.Tags
	-- 		local shouldSuss = true
	-- 		-- for tag in tags do
	-- 		-- 	if tag.value:find("tool") or tag.value:find("weapon") then
	-- 		-- 		MyModGlobal.debugPrint(string.format("Item '%s' is a tool or weapon, skipping", item.Name))
	-- 		-- 		shouldSuss = false
	-- 		-- 		break
	-- 		-- 	end
	-- 		-- end

	-- 		if shouldSuss then
	-- 			local before = #toMove
	-- 			toMove = utils.enqueueSlot(slot, toMove)
	-- 			local after = #toMove
	-- 			MyModGlobal.debugPrint(string.format("Enqueued %d items from the inventory slot %d", after - before,
	-- 				slotid))
	-- 		end
	-- 	end
	-- end

	local openContainers = utils.getOpenContainers()
	for _, container in ipairs(openContainers) do
		local inventories = container.OwnInventories
		MyModGlobal.debugPrint(string.format("Found %d inventories in the open container", #inventories))
		for containerInventory in inventories do
			MyModGlobal.debugPrint(string.format("Enqueuing inventory with %d slots", #containerInventory.slots))
			local before = #toMove
			toMove = utils.enqueueInventory(containerInventory, toMove)
			local after = #toMove
			MyModGlobal.debugPrint(string.format("Enqueued %d items from the open container", after - before))
		end
	end

	local errors = tryMoveItems(toMove, itemTree)
	for _, error in ipairs(errors) do
		MyModGlobal.debugPrint(string.format("Error stacking item: %s", error))
	end
end

local function stackToCursor()
	local slots, err = utils.getSlotsUnderCursor()
	if err then
		MyModGlobal.debugPrint(string.format("Error getting slots under cursor: %s", err))
		return
	end

	local item, slot
	local function predicate(ititem)
		if ititem.Prefab.Identifier.Value == item.Prefab.Identifier.Value then
			if item == ititem then return false end
			-- We are moving items in the predicate because we expect to only
			-- Select a small subset of all items
			-- And it is much easier to let the game decide when we can not move
			-- Any more items (via return value of TryPutItem)
			-- And we then know that we can safely stop
			-- UPDATE: OK well that was a stupid idea, it returns an error for other shit as well
			-- What other shit? Wish I knew
			-- So we'll use HowManyCanBePut instead...
			local moved = slot.inventory.TryPutItem(ititem, slot.slotIndex - 1, false, true, nil)
			if not moved then
				MyModGlobal.debugPrint(string.format("Failed to move item %s to slot %d", ititem.Name, slot
					.slotIndex - 1))
				-- return false, true
			end
			local maxFits = slot.inventory.HowManyCanBePut(ititem.Prefab, slot.slotIndex - 1)
			if maxFits <= 0 then
				MyModGlobal.debugPrint(string.format("Item %s has no more fits in slot %d", ititem.Name, slot
					.slotIndex - 1))
				return false, true
			end
		end
	end

	-- We gotta do a little juggling...
	for _, sslot in ipairs(slots) do
		slot = sslot
		local items
		if not slot.slot.items or #slot.slot.items == 0 then
			MyModGlobal.debugPrint("No items in slot")
			goto continue
		end

		item = slot.slot.items[1]
		MyModGlobal.debugPrint(string.format("Stacking all player items to %s", item.Prefab.Identifier.Value))
		items = {}
		utils.enqueueAllPlayerItems(items, predicate)
		utils.enqueueOpenContainers(items, predicate)
		::continue::
	end
end

local function stackAllToCursor()
	local slots, err = utils.getSlotsUnderCursor()
	if err then
		MyModGlobal.debugPrint(string.format("Error getting slots under cursor: %s", err))
		return
	end

	for _, slot in ipairs(slots) do
		local item, predicate
		if not slot.slot.items or #slot.slot.items == 0 then
			MyModGlobal.debugPrint("No items in slot")
			goto continue
		end

		item = slot.slot.items[1]
		MyModGlobal.debugPrint(string.format("Stacking all items to %s", item.Prefab.Identifier.Value))
		predicate = function(ititem)
			if ititem.Prefab.Identifier.Value == item.Prefab.Identifier.Value then
				if item == ititem then return false end
				-- We are moving items in the predicate because we expect to only
				-- Select a small subset of all items
				-- And it is much easier to let the game decide when we can not move
				-- Any more items (via return value of TryPutItem)
				-- And we then know that we can safely stop
				-- UPDATE: OK well that was a stupid idea, it returns an error for other shit as well
				-- What other shit? Wish I knew
				-- So we'll use HowManyCanBePut instead...
				local moved = slot.inventory.TryPutItem(ititem, slot.slotIndex - 1, false, true, nil)
				if not moved then
					MyModGlobal.debugPrint(string.format("Failed to move item %s to slot %d", ititem.Name, slot
						.slotIndex - 1))
					-- return false, true
				end
				local maxFits = slot.inventory.HowManyCanBePut(ititem.Prefab, slot.slotIndex - 1)
				if maxFits <= 0 then
					MyModGlobal.debugPrint(string.format("Item %s has no more fits in slot %d", ititem.Name, slot
						.slotIndex - 1))
					return false, true
				end
			end
		end

		utils.enqueueAllSubmarineItems({}, predicate)
		utils.enqueueAllPlayerItems({}, predicate)

		::continue::
	end
end

return {
	buildItemTree = buildItemTree,
	tryBuildCharacterItemTree = tryBuildCharacterItemTree,
	sortItemTree = sortItemTree,
	tryMoveItem = tryMoveItem,
	tryMoveItems = tryMoveItems,
	quickStackItems = quickStackItems,
	stackToCursor = stackToCursor,
	stackAllToCursor = stackAllToCursor,
}
