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

---@class Barotrauma.Inventory.ItemSlot
---@field items Barotrauma.Item[]

---@class HollowInventorySlot
---@field inventory? Barotrauma.Inventory
---@field slotIndex1? number Lua based item slots
---@field slotIndex0? number Barotrauma API based item slots
---@field item? Barotrauma.Item
---@field stackSize? number
---@field maxStackSize? number
---@field depth? number Currently almost always 0

-- local globalInventorySlotCache = {}
---@class InventorySlot ---@field slot Barotrauma.Inventory.ItemSlot
---@field inventory Barotrauma.Inventory
---@field slotIndex1 number Lua based item slots
---@field slotIndex0 number Barotrauma API based item slots
---@field item Barotrauma.Item
---@field stackSize number
---@field maxStackSize number
---@field depth number Currently almost always 0
-- ---@field lastUpdated number
MyModGlobal.InventorySlot = {
    ---@param inventory Barotrauma.Inventory
    ---@param slotIndex1 number
    ---@return InventorySlot
    new = function(inventory, slotIndex1)
        local self = setmetatable({}, {
            __index = MyModGlobal.InventorySlot
        })
        self.inventory = inventory
        self.slotIndex1 = slotIndex1
        self.slotIndex0 = slotIndex1 - 1
        self.stackSize = 0
        self.maxStackSize = 0
        self.depth = 0
        -- self:update()

        if inventory and inventory.slots and #inventory.slots > 0 then
            self.slot = inventory.slots[slotIndex1]
        end
        if self.slot and self.slot.items and #self.slot.items > 0 then
            self.item = self.slot.items[1]
            self.stackSize = #self.slot.items
            -- At this point inventory has to exist
            -- If it didn't slot wouldn't either and then this wouldn't either
            self.maxStackSize = self.item.Prefab.GetMaxStackSize(inventory)
        end

        return self
    end,
    --- A very weird builder indeed
    ---@param self InventorySlot
    ---@param other HollowInventorySlot
    with = function(self, other)
        if other.inventory ~= nil then
            self.inventory = other.inventory
        end
        if other.slotIndex1 ~= nil then
            self.slotIndex1 = other.slotIndex1
        end
        if other.slotIndex0 ~= nil then
            self.slotIndex0 = other.slotIndex0
        end
        if other.item ~= nil then
            self.item = other.item
        end
        if other.stackSize ~= nil then
            self.stackSize = other.stackSize
        end
        if other.maxStackSize ~= nil then
            self.maxStackSize = other.maxStackSize
        end
        if other.depth ~= nil then
            self.depth = other.depth
        end
        return self
    end,
    ---@param self InventorySlot
    ---@param item Barotrauma.Item
    pretendMoved = function(self, item)
        if not self.inventory then
            MyModGlobal.debugPrint("Error pretending moved but it was moved to nil inventory")
            return
        end
        -- Slot was previously empty, we want to figure out its max stack for the new item
        if not self.item then
            self.maxStackSize = item.Prefab.GetMaxStackSize(self.inventory)
        end
        self.item = item
        self.stackSize = self.stackSize + 1
    end,
    update = function(self)
        -- self.lastUpdated = Timer.GetTime()
        if not self.inventory then
            MyModGlobal.debugPrint("Error updating inventory slot, inventory not found")
            return
        end
        if not self.inventory.slots then
            MyModGlobal.debugPrint("Error updating inventory slot, inventory has no slots")
            return
        end

        local slot = self.inventory.slots[self.slotIndex1]
        if not slot then
            MyModGlobal.debugPrint("Error updating inventory slot, slot not found")
            return
        end
        self.slot = slot
        if not slot.items or #slot.items == 0 then
            -- MyModGlobal.debugPrint("Error updating inventory slot, slot is empty")
            return
        end
        self.item = slot.items[1]
        self.stackSize = #slot.items
        self.maxStackSize = self.item.Prefab.GetMaxStackSize(self.inventory)
    end,
    __tostring = function(self)
        return string.format(
            "InventorySlot(inventory=%s, item=%s, stackSize=%d, maxStackSize=%d, slotIndex1=%d, slotIndex0=%d)",
            tostring(self.inventory), tostring(self.item), self.stackSize, self.maxStackSize, self.slotIndex1,
            self.slotIndex0)
    end,
    ---@param self InventorySlot
    ---@param predicate? fun(slot: InventorySlot): boolean
    ---@return InventorySlot[]
    getNearbySlots = function(self, predicate)
        predicate = predicate or function() return true end

        local slotsPerRow = 900
        local ok, err = pcall(function()
            slotsPerRow = self.inventory.slotsPerRow
        end)
        if not ok then
            MyModGlobal.debugPrint(string.format("Error getting slots per row: %s", err))
        end

        local getGridPos = function(slotIndex)
            local x = slotIndex % slotsPerRow
            local y = math.floor(slotIndex / slotsPerRow)
            return x, y
        end

        local slots = {}
        for slotIndex, _ in ipairs(self.inventory.slots) do
            local inventorySlot = MyModGlobal.InventorySlot.new(self.inventory, slotIndex)
            if predicate(inventorySlot) then
                slots[#slots + 1] = inventorySlot
            end
        end

        local slotx, sloty = getGridPos(self.slotIndex0)
        table.sort(slots, function(a, b)
            local ax, ay = getGridPos(a.slotIndex0)
            local bx, by = getGridPos(b.slotIndex0)

            -- Chebyshev distance
            local distA = math.max(math.abs(ax - slotx), math.abs(ay - sloty))
            local distB = math.max(math.abs(bx - slotx), math.abs(by - sloty))

            if distA == distB then
                return a.slotIndex0 < b.slotIndex0
            end
            return distA < distB
        end)

        return slots
    end,
    --- TODO: What about item condition?
    ---@param self InventorySlot
    ---@param itemPrefab Barotrauma.ItemPrefab
    ---@return number
    howManyCanFit = function(self, itemPrefab)
        -- There is an item in the slot and it's not stackable with itemPrefab
        if self.item and not self.item.Prefab.Equals(itemPrefab) then
            return 0
        end
        -- The slot is empty - we can fit as many as the game tells us
        if not self.item then
            return itemPrefab.GetMaxStackSize(self.inventory)
        end
        -- The slot has an item that is stackable with itemPrefab
        -- We can fit as many as to fill the stack
        return self.maxStackSize - self.stackSize
    end,
    ---@param self InventorySlot
    ---@param itemPrefab Barotrauma.ItemPrefab
    ---@return boolean
    canFit = function(self, itemPrefab)
        return self:howManyCanFit(itemPrefab) > 0
    end,
    ---@param self InventorySlot
    ---@return number
    maxFits = function(self)
        return self.maxStackSize - self.stackSize
    end
    -- hash = function(self)
    --     return string.format("%s:%d:%d", tostring(self.inventory), self.slotIndex1, self.slotIndex0)
    -- end
}

---@class ItemMoveRequest
---@field what Barotrauma.Item
---@field where InventorySlot
---@field allowSwap boolean
---@field allowCombine boolean

local enqueueMove
do
    -- A bit of cheeky scoping
    local enabled = true
    ---@type ItemMoveRequest[]
    local itemMoveQueue = {}
    ---@type table<Barotrauma.Item, boolean>
    local itemLookup = {}
    local rate = 500
    local perIteration = 30
    local noQueue = true
    -- rate / 1000 is ms to seconds and *perIteraion is number of items per second
    local maxQueueSize = 10 * (1000 / rate * perIteration)
    local function processQueue()
        if noQueue then return end
        -- MyModGlobal.debugPrint("Processing queue")
        Timer.Wait(processQueue, rate)
        if not enabled then return end
        if #itemMoveQueue == 0 then return end
        local iterations = math.min(perIteration, #itemMoveQueue)
        for _ = 1, iterations do
            ---@type ItemMoveRequest
            local moveRequest = table.remove(itemMoveQueue, 1)

            -- TODO: Maybe try and figure out if we CAN put A into B
            moveRequest.allowCombine = moveRequest.allowCombine or false
            moveRequest.allowSwap = moveRequest.allowSwap or false
            local success = moveRequest.where.inventory.TryPutItem(moveRequest.what, moveRequest.where.slotIndex0,
                moveRequest.allowSwap, moveRequest.allowCombine, nil)
            if not success then
                MyModGlobal.debugPrint(string.format("Failed moving item from %s to %s", tostring(moveRequest.what),
                    tostring(moveRequest.where:__tostring())))
            end
            itemLookup[moveRequest.what] = nil
        end
    end
    processQueue()

    ---@param what Barotrauma.Item
    ---@param where InventorySlot
    ---@param allowSwap? boolean
    ---@param allowCombine? boolean
    enqueueMove = function(what, where, allowSwap, allowCombine)
        allowCombine = allowCombine == true
        allowSwap = allowSwap == true
        if noQueue then
            local success = where.inventory.TryPutItem(what, where.slotIndex0,
                allowSwap, allowCombine, nil)
            if not success then
                MyModGlobal.debugPrint(string.format("Failed moving item from %s to %s", tostring(what),
                    tostring(where:__tostring())))
            end
            where:pretendMoved(what)
        else
            if #itemMoveQueue >= maxQueueSize then
                MyModGlobal.debugPrint("Queue is full, skipping move")
                return
            end
            if itemLookup[what] then
                MyModGlobal.debugPrint("Item is already in the queue, skipping move")
                return
            end
            MyModGlobal.debugPrint(string.format("Enqueuing move from %s to %s, now in queue %d/%d", tostring(what),
                tostring(where:__tostring()), #itemMoveQueue, maxQueueSize))
            table.insert(itemMoveQueue, {
                what = what,
                where = where,
                allowSwap = allowSwap or false,
                allowCombine = allowCombine ~= false,
            })
            itemLookup[what] = true
            -- We will very optimistically pretend that this will 100% for sure work
            where:pretendMoved(what)
        end
    end
end


---@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()
    ---@diagnostic disable-next-line: return-type-mismatch
    if err then return nil, err end
    ---@diagnostic disable-next-line: return-type-mismatch
    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
-- TODO: Rework these enqueue functions to accept a params object
-- That will house all optional parameters
-- And in that include recurse boolean

---@class ItemRefs
---@field item Barotrauma.Item
---@field inventory Barotrauma.Inventory
---@field slot Barotrauma.Inventory.ItemSlot
---@field slotIndex1 number

---@class EnqueueOptions
---@field itemQueue? Barotrauma.Item[]
---@field slotQueue? Barotrauma.Inventory.ItemSlot[]
---@field inventoryQueue? Barotrauma.Inventory[]
---@field itemPredicate? fun(item: Barotrauma.Item, itemRef: ItemRefs): boolean
---@field slotPredicate? fun(slot: Barotrauma.Inventory.ItemSlot, itemRef: ItemRefs): boolean
---@field inventoryPredicate? fun(inventory: Barotrauma.Inventory, itemRef: ItemRefs): boolean
---@field loadRefs? boolean
---@field itemRef? ItemRefs
---@field recurse? boolean

---@param options EnqueueOptions
---@return EnqueueOptions
local function ensureOptionsDefaults(options)
    options = options or {}
    options.itemQueue = options.itemQueue or {}
    options.slotQueue = options.slotQueue or {}
    options.inventoryQueue = options.inventoryQueue or {}
    options.itemPredicate = options.itemPredicate or function() return true end
    options.slotPredicate = options.slotPredicate or function() return true end
    options.inventoryPredicate = options.inventoryPredicate or function() return true end
    options.loadRefs = options.loadRefs == true
    options.itemRef = options.itemRef or {}
    options.recurse = options.recurse == true
    return options
end

local enqueueItem
local enqueueSlot
local enqueueInventory
local enqueuePlayerItems
local enqueueOpenContainers
local enqueueSubmarineItems
local enqueueAllOwnedItems

do
    ---@param item Barotrauma.Item
    ---@param options EnqueueOptions
    ---@return EnqueueOptions, string?
    enqueueItem = function(item, options)
        options = ensureOptionsDefaults(options)
        if not item then return options, "No item" end

        local ok, stop = options.itemPredicate(item, options.itemRef)
        if ok then
            options.itemQueue[#options.itemQueue + 1] = item
        end
        if stop then return options, "Stop" end

        local err
        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
            if options.recurse then
                if options.loadRefs then
                    options.itemRef.item = item
                    options.inventoryQueue, err = enqueueInventory(item.OwnInventory, options)
                else
                    options.inventoryQueue, err = enqueueInventory(item.OwnInventory, options)
                end
            end
        end
        return options, err
    end

    ---@param slot Barotrauma.Inventory.ItemSlot
    ---@param options EnqueueOptions
    ---@return EnqueueOptions, string?
    enqueueSlot = function(slot, options)
        options = ensureOptionsDefaults(options)
        if not slot then return options, "No slot" end
        if not slot.items then return options, "No items" end

        local ok, stop = options.slotPredicate(slot, options.itemRef)
        if ok then
            options.slotQueue[#options.slotQueue + 1] = slot
        end
        if stop then return options, "Stop" end

        for _, item in ipairs(slot.items) do
            -- We redeclare err every iteration so it doesn't spill over
            local err
            if options.loadRefs then
                options.itemRef.slot = slot
                options, err = enqueueItem(item, options)
            else
                options, err = enqueueItem(item, options)
            end
            if err then
                return options, err
            end
        end
        return options
    end

    ---@param inventory Barotrauma.Inventory
    ---@param options EnqueueOptions
    ---@return EnqueueOptions, string?
    enqueueInventory = function(inventory, options)
        options = ensureOptionsDefaults(options)
        if not inventory then return options, "No inventory" end
        if not inventory.slots then return options, "No slots" end

        local ok, stop = options.inventoryPredicate(inventory, options.itemRef)
        if ok then
            options.inventoryQueue[#options.inventoryQueue + 1] = inventory
        end
        if stop then return options, "Stop" end

        for i, slot in ipairs(inventory.slots) do
            local err
            if options.loadRefs then
                options.itemRef.inventory = inventory
                options.itemRef.slot = slot
                options.itemRef.slotIndex1 = i
                options, err = enqueueSlot(slot, options)
            else
                options, err = enqueueSlot(slot, options)
            end
            if err then
                return options, err
            end
        end
        return options
    end

    local relevantPlayerInventorySlots = {
        false,
        false,
        false,
        false,
        false,
        true,
        true,
        true,
        true,
        true,
        true,
        true,
        true,
        true,
        true,
        true,
        true,
        true,
    }
    ---@param options EnqueueOptions
    ---@return EnqueueOptions, string?
    enqueuePlayerItems = function(options)
        options = ensureOptionsDefaults(options)

        local character = Character.Controlled
        if not character then return options, "No character" end
        local inventory = character.Inventory
        if not inventory then return options, "No inventory" end

        options.loadRefs = true
        local originalItemPredicate = options.itemPredicate or function() return true end
        options.itemPredicate = function(item)
            if not item then return false end
            local parentInventory = item.ParentInventory
            if not parentInventory then return false end
            if not parentInventory.Equals(inventory) then return false end
            return originalItemPredicate(item, options.itemRef)
        end
        local originalSlotPredicate = options.slotPredicate or function() return true end
        options.slotPredicate = function(slot, itemRef)
            if not slot then return false end
            if itemRef.slotIndex1 and relevantPlayerInventorySlots[itemRef.slotIndex1] then
                return originalSlotPredicate(slot, itemRef)
            end
            return false
        end

        local err
        options, err = enqueueInventory(inventory, options)
        if err then return options, err end

        return options
    end

    ---@param options EnqueueOptions
    ---@return EnqueueOptions, string?
    enqueueOpenContainers = function(options)
        options = ensureOptionsDefaults(options)

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

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

        return options
    end

    ---@param options EnqueueOptions
    ---@return EnqueueOptions, string?
    enqueueSubmarineItems = function(options)
        options = ensureOptionsDefaults(options)

        -- This only exists so predicate does not explode
        -- Even if its empty
        local itemRef = {}

        local character = Character.Controlled
        if not character then return options, "No character" end
        local submarine = character.Submarine
        if not submarine then return options, "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 = options.itemPredicate(item, itemRef)
            if ok then
                options.itemQueue[#options.itemQueue + 1] = item
            end
            if stop then return options, "Stop" end
        end

        return options
    end

    ---@param options EnqueueOptions
    ---@return EnqueueOptions, string?
    enqueueAllOwnedItems = function(options)
        options = ensureOptionsDefaults(options)

        local err
        options, err = enqueuePlayerItems(options)
        if err then return options, err end

        options, err = enqueueSubmarineItems(options)
        if err then return options, err end

        return options
    end
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 slots, "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
            local inventorySlot = MyModGlobal.InventorySlot.new(inventory, i)
            slots[#slots + 1] = inventorySlot
        end

        ::continue::
    end

    return slots, nil
end

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

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

    local err
    slots, err = getMouseoverSlots(inventory, slots)
    if err then return slots, 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
            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
                    local inventorySlot = MyModGlobal.InventorySlot.new(containerInventory, i)
                    slots[#slots + 1] = inventorySlot
                end
            end
            ::continue::
        end
    end

    return slots, nil
end

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

return {
    enqueueItem = enqueueItem,
    enqueueSlot = enqueueSlot,
    enqueueInventory = enqueueInventory,
    enqueuePlayerItems = enqueuePlayerItems,
    enqueueSubmarineItems = enqueueSubmarineItems,
    enqueueAllOwnedItems = enqueueAllOwnedItems,
    enqueueOpenContainers = enqueueOpenContainers,
    getOpenContainers = getOpenContainers,
    getFirstOpenContainer = getFirstOpenContainer,
    getSlotsUnderCursor = getSlotsUnderCursor,
    getFirstSlotUnderCursor = getFirstSlotUnderCursor,
    enqueueMove = enqueueMove,
}
