-- luacheck: globals Character MyModGlobal
-- luacheck: max line length 420

---@class ItemRefs
---@field item Barotrauma.Item
---@field inventory Barotrauma.ItemInventory
---@field slot Barotrauma.ItemInventory.Slot

---@class InventorySlot
---@field slot Barotrauma.ItemSlot
---@field inventory Barotrauma.ItemInventory
---@field slotIndex number

---@return Barotrauma.Item[], string?
local function getOpenContainers()
    local controlledCharacter = Character.Controlled
    if not controlledCharacter then return {}, "No controlled character" end
    local selectedItem = controlledCharacter.SelectedItem
    if not selectedItem then return {}, "No selected item" end
    return { selectedItem }, nil
end

---@return Barotrauma.Item, string?
local function getFirstOpenContainer()
    local containers, err = getOpenContainers()
    if err then return nil, err end
    if #containers == 0 then return nil, "No open containers" end
    return containers[1], nil
end

-- We got to do this shit because enqueueInventory calls enqueueItem
-- And enqueueItem calls enqueueInventory
-- So unless we define them both before using them
-- We will get an error saying either is undefined
local enqueueItem
local enqueueSlot
local enqueueInventory
local enqueueOpenContainers
local allPlayerItems
local allSubmarineItems
local allOwnedItems
local _

---@alias FilterPredicate fun(item: Barotrauma.Item, inventoryRef?: Barotrauma.ItemInventory, slotRef: Barotrauma.ItemInventory.Slot): boolean

-- Loading refs is optional because it MAY have a performance impact

---@param item Barotrauma.Item
---@param queue Barotrauma.Item[]
---@param predicate? FilterPredicate
---@param loadRefs? boolean
---@param itemRef? ItemRefs
---@return Barotrauma.Item[], string?
enqueueItem = function(item, queue, predicate, loadRefs, itemRef)
    queue = queue or {}
    predicate = predicate or function() return true end
    itemRef = itemRef or {}
    -- debugPrint(string.format("Enqueuing item: %s", item.Prefab.Identifier.Value))
    -- local err
    -- This should make it breadth first, right...?
    -- No, not yet...
    if not item then return queue, "No item" end

    local ok, stop = predicate(item, itemRef)
    if ok then
        queue[#queue + 1] = item
    end
    if stop then return queue, "Stop" end
    if item.OwnInventory then
        -- As far as I know every item has only one inventory
        -- Only machines have multiple
        -- So inventrorY should be fine here
        -- debugPrint("Item has its own inventory, enqueuing inventory...")
        if loadRefs then
            itemRef.item = item
            queue, _ = enqueueInventory(item.OwnInventory, queue, predicate, loadRefs, itemRef)
        else
            queue, _ = enqueueInventory(item.OwnInventory, queue, predicate, itemRef)
        end
        -- if err then
        --     debugPrint(string.format("Error enqueuing inventory: %s", err))
        -- end
    end
    -- debugPrint(string.format("Item enqueued. Current queue size: %d", #queue))
    return queue, nil
end

---@param slot Barotrauma.ItemInventory.Slot
---@param queue Barotrauma.Item[]
---@param predicate? FilterPredicate
---@param loadRefs? boolean
---@param itemRef? ItemRefs
---@return Barotrauma.Item[], string?
enqueueSlot = function(slot, queue, predicate, loadRefs, itemRef)
    queue = queue or {}
    predicate = predicate or function() return true end
    itemRef = itemRef or {}
    -- debugPrint(string.format("Enqueuing slot with %d items.", #slot.items))
    -- We don't want to shadow queue
    local err
    -- If the slot is empty there's nothing to iterate
    -- And we will naturally return queue as is
    if not slot then return queue, "No slot" end
    if not slot.items then return queue, "No items" end

    for _, item in ipairs(slot.items) do
        -- Only the final leaf nodes decide upon the predicate
        if loadRefs then
            itemRef.slot = slot
            queue, err = enqueueItem(item, queue, predicate, loadRefs, itemRef)
        else
            queue, err = enqueueItem(item, queue, predicate)
        end
        if err then
            return queue, err
        end
    end
    -- debugPrint(string.format("Finished enqueuing slot. Current queue size: %d", #queue))
    return queue
end

---@param inventory Barotrauma.ItemInventory
---@param queue Barotrauma.Item[]
---@param predicate? FilterPredicate
---@param loadRefs? boolean
---@param itemRef? ItemRefs
---@return Barotrauma.Item[], string?
enqueueInventory = function(inventory, queue, predicate, loadRefs, itemRef)
    queue = queue or {}
    predicate = predicate or function() return true end
    itemRef = itemRef or {}
    -- debugPrint(string.format("Enqueuing inventory with %d slots.", #inventory.slots))
    local err
    if not inventory then return queue, "No inventory" end
    if not inventory.slots then return queue, "No slots" end

    for _, slot in ipairs(inventory.slots) do
        -- Only the final leaf nodes decide upon the predicate
        if loadRefs then
            itemRef.inventory = inventory
            queue, err = enqueueSlot(slot, queue, predicate, loadRefs, itemRef)
        else
            queue, err = enqueueSlot(slot, queue, predicate)
        end
        if err then
            return queue, err
        end
    end
    -- debugPrint(string.format("Finished enqueuing inventory. Current queue size: %d", #queue))
    return queue
end

local relevantPlayerInventorySlots = { 6, 7, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, MyModGlobal.BAG_SLOT }
---@param queue Barotrauma.Item[]
---@param predicate? FilterPredicate
---@param loadRefs? boolean
---@return Barotrauma.Item[], string?
allPlayerItems = function(queue, predicate, loadRefs)
    queue = queue or {}
    predicate = predicate or function() return true end

    local character = Character.Controlled
    if not character then return queue, "No character" end

    local inventory = character.Inventory
    if not inventory then return queue, "No inventory" end

    for _, slotid in ipairs(relevantPlayerInventorySlots) do
        local slot = inventory.slots[slotid]
        local err

        if not slot then goto continue end
        if #slot.items == 0 then goto continue end

        queue, err = enqueueSlot(slot, queue, predicate, loadRefs)
        if err then return queue, err end

        ::continue::
    end

    return queue
end

---@param queue Barotrauma.Item[]
---@param predicate? FilterPredicate
---@param loadRefs? boolean
---@return Barotrauma.Item[], string?
enqueueOpenContainers = function(queue, predicate, loadRefs)
    queue = queue or {}
    predicate = predicate or function() return true end

    local containers, err = getOpenContainers()
    if err then return queue, err end

    for _, container in ipairs(containers) do
        local inventories = container.OwnInventories
        if not inventories then goto continue end
        for containerInventory in inventories do
            queue, err = enqueueInventory(containerInventory, queue, predicate, loadRefs)
            if err then return queue, err end
        end
        ::continue::
    end

    return queue
end

---@param queue Barotrauma.Item[]
---@param predicate? FilterPredicate
---@return Barotrauma.Item[], string?
allSubmarineItems = function(queue, predicate)
    queue = queue or {}
    predicate = predicate or function() return true end
    -- This only exists so predicate does not explode
    -- Even if its empty
    local itemRef = {}

    local character = Character.Controlled
    if not character then return queue, "No character" end

    local submarine = character.Submarine
    if not submarine then return queue, "No submarine" end

    for item in submarine.GetItems(false) do
        -- We do NOT want to call enqueueItem here because enqueueItem
        -- Is recursive
        -- And this call (GetItems) already gets all items
        -- So we would be doing double the work (at best case)
        -- It also means we won't have refs here which sucks
        local ok, stop = predicate(item, itemRef)
        if ok then
            queue[#queue + 1] = item
        end
        if stop then return queue, "Stop" end
    end

    return queue
end

---@param queue Barotrauma.Item[]
---@param predicate? FilterPredicate
---@param loadRefs? boolean
---@return Barotrauma.Item[], string?
allOwnedItems = function(queue, predicate, loadRefs)
    queue = queue or {}
    predicate = predicate or function() return true end

    local err
    queue, err = allPlayerItems(queue, predicate, loadRefs)
    if err then return queue, err end

    queue, err = allSubmarineItems(queue, predicate)
    if err then return queue, err end

    return queue
end

-- There is actually no need to recurse deep
-- Because we can only have an item in the inventory open
-- And not an item in an item in the inventory
-- So in theory we only need to recurse 1 deep
---@param inventory Barotrauma.Inventory
---@param slots InventorySlot[]
---@param depth number
---@return InventorySlot[], string?
local function getMouseoverSlots(inventory, slots, depth)
    slots = slots or {}
    depth = depth or 0
    if depth > 1 then return slots, nil end

    local visualSlots = inventory.visualSlots
    if not visualSlots then return nil, "Inventory has no visual slots" end

    for i, visualSlot in ipairs(visualSlots) do
        local item
        local itemInventory
        -- local err

        local slot = inventory.slots[i]
        if not slot then
            -- MyModGlobal.debugPrint("Slot is not a valid slot")
            goto continue
        end

        if #slot.items == 0 then
            goto mouseover
        end

        item = slot.items[1]
        if not item then
            goto mouseover
        end

        itemInventory = item.OwnInventory
        if not itemInventory then
            goto mouseover
        end

        -- print("Before: " .. #slots)--
        getMouseoverSlots(itemInventory, slots, depth + 1)
        -- if err then
        -- 	MyModGlobal.debugPrint(string.format("Error getting mouseover slots: %s", err))
        -- end
        -- print("After: " .. #slots)

        ::mouseover::
        if visualSlot:MouseOn() then
            slots[#slots + 1] = {
                inventory = inventory,
                slotIndex = i,
                slot = slot
            }
        end

        ::continue::
    end

    return slots, nil
end

---@return InventorySlot[], string?
local function getSlotsUnderCursor()
    -- Make sure we have a controlled character
    local controlledCharacter = Character.Controlled
    if not controlledCharacter then return nil, "No controlled character" end

    local inventory = controlledCharacter.Inventory
    if not inventory then return nil, "No inventory" end

    local mouseoverSlots, err = getMouseoverSlots(inventory)
    if err then return mouseoverSlots, err end

    -- Even if we don't get them we're still fine
    local openContainers, _ = getOpenContainers()
    -- if err then return mouseoverSlots, err end

    for _, container in ipairs(openContainers) do
        local containerInventories = container.OwnInventories
        for containerInventory in containerInventories do
            local slot
            if not containerInventory or not containerInventory.visualSlots then
                MyModGlobal.debugPrint("Container inventory has no visual slots")
                goto continue
            end
            for i, visualSlot in ipairs(containerInventory.visualSlots) do
                if visualSlot:MouseOn() then
                    slot = containerInventory.slots[i]
                    mouseoverSlots[#mouseoverSlots + 1] = {
                        inventory = containerInventory,
                        slotIndex = i,
                        slot = slot
                    }
                end
            end
            ::continue::
        end
    end

    return mouseoverSlots, nil
end

---@return InventorySlot, string?
local function getFirstSlotUnderCursor()
    local slots, err = getSlotsUnderCursor()
    if err then return nil, err end
    if #slots == 0 then return nil, "No slots found under cursor" end
    for _, slot in ipairs(slots) do
        if slot.slot.items and #slot.slot.items > 0 then
            return slot
        end
    end
    return slots[1]
end

return {
    enqueueItem = enqueueItem,
    enqueueSlot = enqueueSlot,
    enqueueInventory = enqueueInventory,
    enqueueAllPlayerItems = allPlayerItems,
    enqueueAllSubmarineItems = allSubmarineItems,
    enqueueAllOwnedItems = allOwnedItems,
    enqueueOpenContainers = enqueueOpenContainers,
    getOpenContainers = getOpenContainers,
    getFirstOpenContainer = getFirstOpenContainer,
    getSlotsUnderCursor = getSlotsUnderCursor,
    getFirstSlotUnderCursor = getFirstSlotUnderCursor,
}
