-- GearBagSort: Sorts a designated bag by item level after Blizzard bag sort
-- Author: Jamie Kaos
-- Version: 1.3.0
--
-- Uses live selection-sort with event-driven swap confirmation.
-- Waits for ITEM_LOCK_CHANGED to verify each swap landed before proceeding.

local ADDON_NAME = "GearBagSort"

local defaults = {
    gearBagIndex = 1,
    sortDescending = true,
    autoSort = true,
    debug = false,
}

local db
local isSorting = false

---------------------------------------------------------------------------
-- Bag helpers
---------------------------------------------------------------------------

local function UserBagToGameBag(userBag)
    if userBag == 5 then return 0 end
    return userBag
end

local function GetBagName(userBag)
    if userBag == 5 then return "Backpack" end
    local gameBag = UserBagToGameBag(userBag)
    local invSlot = 19 + gameBag
    local itemLink = GetInventoryItemLink("player", invSlot)
    if itemLink then
        local name = C_Item.GetItemInfo(itemLink)
        return name or itemLink
    end
    return "Bag Slot " .. userBag
end

---------------------------------------------------------------------------
-- Print helpers
---------------------------------------------------------------------------

local function Print(msg)
    print("|cff00ccff[GearBagSort]|r " .. msg)
end

local function DebugPrint(msg)
    if db and db.debug then
        print("|cff888888[GBS Debug]|r " .. msg)
    end
end

---------------------------------------------------------------------------
-- Item level reading
---------------------------------------------------------------------------

local function GetReliableItemLevel(bagID, slot, itemInfo)
    local ilvl = 0

    local itemLoc = ItemLocation:CreateFromBagAndSlot(bagID, slot)
    if C_Item.DoesItemExist(itemLoc) then
        ilvl = C_Item.GetCurrentItemLevel(itemLoc) or 0
    end

    if ilvl == 0 and itemInfo.hyperlink then
        local effectiveILvl = GetDetailedItemLevelInfo(itemInfo.hyperlink)
        if effectiveILvl and effectiveILvl > 0 then
            ilvl = effectiveILvl
        end
    end

    if ilvl == 0 then
        local _, _, _, baseILvl = C_Item.GetItemInfo(itemInfo.hyperlink or itemInfo.itemID)
        if baseILvl and baseILvl > 0 then
            ilvl = baseILvl
        end
    end

    return ilvl
end

local function ReadSlot(bagID, slot)
    local itemInfo = C_Container.GetContainerItemInfo(bagID, slot)
    if itemInfo and itemInfo.itemID then
        local ilvl = GetReliableItemLevel(bagID, slot, itemInfo)
        local _, _, itemQuality = C_Item.GetItemInfo(itemInfo.hyperlink or itemInfo.itemID)
        return {
            ilvl = ilvl,
            quality = itemQuality or 0,
            itemID = itemInfo.itemID,
            itemLink = itemInfo.hyperlink,
            isEmpty = false,
        }
    else
        return {
            ilvl = -1,
            quality = -1,
            itemID = nil,
            itemLink = nil,
            isEmpty = true,
        }
    end
end

---------------------------------------------------------------------------
-- Comparison
---------------------------------------------------------------------------

local function ShouldBeFirst(a, b, descending)
    if a.isEmpty and not b.isEmpty then return false end
    if not a.isEmpty and b.isEmpty then return true end
    if a.isEmpty and b.isEmpty then return false end

    if a.ilvl ~= b.ilvl then
        if descending then
            return a.ilvl > b.ilvl
        else
            return a.ilvl < b.ilvl
        end
    end
    if a.quality ~= b.quality then
        return a.quality > b.quality
    end
    return (a.itemID or 0) < (b.itemID or 0)
end

---------------------------------------------------------------------------
-- Event-driven swap system
---------------------------------------------------------------------------

local swapFrame = CreateFrame("Frame")
local pendingSwap = nil       -- {bagID, fromSlot, toSlot, callback}
local swapTimeout = nil

-- When we pick up + place an item, the slots get locked then unlocked.
-- We listen for the unlock to know the swap is done.
local lockCount = 0

local function OnItemLockChanged(bagOrEquip, slotIndex)
    if not pendingSwap then return end

    -- We only care about lock changes in our bag
    if bagOrEquip ~= pendingSwap.bagID then return end
    if slotIndex ~= pendingSwap.fromSlot and slotIndex ~= pendingSwap.toSlot then return end

    -- Check if BOTH slots are now unlocked
    -- GetContainerItemInfo returns a table in modern WoW
    local fromInfo = C_Container.GetContainerItemInfo(pendingSwap.bagID, pendingSwap.fromSlot)
    local toInfo = C_Container.GetContainerItemInfo(pendingSwap.bagID, pendingSwap.toSlot)
    local fromIsLocked = fromInfo and fromInfo.isLocked
    local toIsLocked = toInfo and toInfo.isLocked

    if not fromIsLocked and not toIsLocked then
        -- Swap is confirmed complete
        local cb = pendingSwap.callback
        pendingSwap = nil
        if swapTimeout then
            swapTimeout:Cancel()
            swapTimeout = nil
        end
        if cb then cb(true) end
    end
end

swapFrame:SetScript("OnEvent", function(self, event, ...)
    if event == "ITEM_LOCK_CHANGED" then
        OnItemLockChanged(...)
    end
end)

-- Perform a single swap with confirmation
local function DoConfirmedSwap(bagID, fromSlot, toSlot, callback)
    pendingSwap = {
        bagID = bagID,
        fromSlot = fromSlot,
        toSlot = toSlot,
        callback = callback,
    }

    swapFrame:RegisterEvent("ITEM_LOCK_CHANGED")

    -- Do the swap
    ClearCursor()
    C_Container.PickupContainerItem(bagID, fromSlot)
    C_Container.PickupContainerItem(bagID, toSlot)
    ClearCursor()

    -- Safety timeout: if we don't get lock events within 2 seconds, proceed anyway
    swapTimeout = C_Timer.NewTimer(2.0, function()
        if pendingSwap then
            DebugPrint(string.format("Swap timeout for slots %d<->%d, proceeding...", fromSlot, toSlot))
            swapFrame:UnregisterEvent("ITEM_LOCK_CHANGED")
            local cb = pendingSwap.callback
            pendingSwap = nil
            swapTimeout = nil
            if cb then cb(false) end
        end
    end)
end

---------------------------------------------------------------------------
-- Live selection sort with confirmed swaps
---------------------------------------------------------------------------

local sortBagID
local sortSlotIndex
local sortNumSlots
local sortSwapCount

local function DoNextSelectionStep()
    if not isSorting then return end

    if sortSlotIndex >= sortNumSlots then
        isSorting = false
        swapFrame:UnregisterEvent("ITEM_LOCK_CHANGED")
        Print("Sort complete! (" .. sortSwapCount .. " swaps)")
        return
    end

    -- Read current slot fresh
    local currentItem = ReadSlot(sortBagID, sortSlotIndex)

    -- Find the best item from remaining slots
    local bestSlot = sortSlotIndex
    local bestItem = currentItem

    for checkSlot = sortSlotIndex + 1, sortNumSlots do
        local checkItem = ReadSlot(sortBagID, checkSlot)
        if ShouldBeFirst(checkItem, bestItem, db.sortDescending) then
            bestSlot = checkSlot
            bestItem = checkItem
        end
    end

    if bestSlot ~= sortSlotIndex then
        DebugPrint(string.format("Slot %d: swap in ilvl %d %s (from slot %d)",
            sortSlotIndex, bestItem.ilvl, bestItem.itemLink or "?", bestSlot))

        sortSwapCount = sortSwapCount + 1

        DoConfirmedSwap(sortBagID, bestSlot, sortSlotIndex, function(success)
            swapFrame:UnregisterEvent("ITEM_LOCK_CHANGED")
            if not success then
                DebugPrint("Swap may not have completed cleanly, adding extra delay...")
            end
            -- Small extra delay after confirmation to let the UI update
            C_Timer.After(0.05, function()
                sortSlotIndex = sortSlotIndex + 1
                DoNextSelectionStep()
            end)
        end)
    else
        -- Already correct, move on
        sortSlotIndex = sortSlotIndex + 1
        -- Use After(0) to avoid stack overflow on long bags
        C_Timer.After(0, DoNextSelectionStep)
    end
end

---------------------------------------------------------------------------
-- Wait for item data
---------------------------------------------------------------------------

local MAX_CACHE_RETRIES = 15
local cacheRetries = 0

local function AllItemsLoaded(bagID)
    local numSlots = C_Container.GetContainerNumSlots(bagID)
    for slot = 1, numSlots do
        local itemInfo = C_Container.GetContainerItemInfo(bagID, slot)
        if itemInfo and itemInfo.itemID then
            local name = C_Item.GetItemInfo(itemInfo.itemID)
            if not name then return false end
            local ilvl = GetReliableItemLevel(bagID, slot, itemInfo)
            if ilvl == 0 then return false end
        end
    end
    return true
end

local function RequestAllItemData(bagID)
    local numSlots = C_Container.GetContainerNumSlots(bagID)
    for slot = 1, numSlots do
        local itemInfo = C_Container.GetContainerItemInfo(bagID, slot)
        if itemInfo and itemInfo.itemID then
            C_Item.RequestLoadedData(itemInfo.itemID)
        end
    end
end

local function WaitForDataThenSort(bagID)
    if AllItemsLoaded(bagID) or cacheRetries >= MAX_CACHE_RETRIES then
        if cacheRetries >= MAX_CACHE_RETRIES then
            DebugPrint("Item data didn't fully load, proceeding anyway...")
        else
            DebugPrint("All item data loaded.")
        end
        cacheRetries = 0

        if db.debug then
            local numSlots = C_Container.GetContainerNumSlots(bagID)
            DebugPrint("Bag contents before sort:")
            for slot = 1, numSlots do
                local item = ReadSlot(bagID, slot)
                if not item.isEmpty then
                    DebugPrint(string.format("  Slot %d: ilvl %d q%d - %s",
                        slot, item.ilvl, item.quality, item.itemLink or "?"))
                end
            end
        end

        sortBagID = bagID
        sortSlotIndex = 1
        sortNumSlots = C_Container.GetContainerNumSlots(bagID)
        sortSwapCount = 0
        DoNextSelectionStep()
    else
        cacheRetries = cacheRetries + 1
        DebugPrint(string.format("Waiting for item data... (%d/%d)", cacheRetries, MAX_CACHE_RETRIES))
        RequestAllItemData(bagID)
        C_Timer.After(0.3, function()
            WaitForDataThenSort(bagID)
        end)
    end
end

local function SortGearBag()
    if isSorting then
        Print("Already sorting, please wait...")
        return
    end

    local bagID = UserBagToGameBag(db.gearBagIndex)
    local numSlots = C_Container.GetContainerNumSlots(bagID)

    if not numSlots or numSlots == 0 then
        Print("Gear bag (slot " .. db.gearBagIndex .. ") appears to be empty or not equipped.")
        return
    end

    Print("Sorting " .. GetBagName(db.gearBagIndex) .. " by item level (" ..
        (db.sortDescending and "highest first" or "lowest first") .. ")...")

    isSorting = true
    cacheRetries = 0
    WaitForDataThenSort(bagID)
end

---------------------------------------------------------------------------
-- Hook Blizzard sort
---------------------------------------------------------------------------

local blizzardSortPending = false

local function OnBagSortFinished()
    if blizzardSortPending and db.autoSort then
        blizzardSortPending = false
        C_Timer.After(1.5, function()
            DebugPrint("Blizzard sort finished, auto-sorting gear bag...")
            SortGearBag()
        end)
    end
end

hooksecurefunc(C_Container, "SortBags", function()
    if db and db.autoSort and not isSorting then
        DebugPrint("Blizzard SortBags detected...")
        blizzardSortPending = true
    end
end)

---------------------------------------------------------------------------
-- Options panel
---------------------------------------------------------------------------

local function CreateOptionsPanel()
    local panel = CreateFrame("Frame", "GearBagSortOptionsPanel")
    panel.name = ADDON_NAME

    local title = panel:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
    title:SetPoint("TOPLEFT", 16, -16)
    title:SetText("|cff00ccffGearBagSort|r")

    local version = panel:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    version:SetPoint("LEFT", title, "RIGHT", 8, 0)
    version:SetText("v1.3.0")

    local subtitle = panel:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    subtitle:SetPoint("TOPLEFT", title, "BOTTOMLEFT", 0, -8)
    subtitle:SetText("Sort a designated bag by item level alongside Blizzard's default bag sorting.")

    local yOffset = -80

    local bagLabel = panel:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    bagLabel:SetPoint("TOPLEFT", panel, "TOPLEFT", 16, yOffset)
    bagLabel:SetText("Gear Bag:")

    local bagDropdown = CreateFrame("Frame", "GBSBagDropdown", panel, "UIDropDownMenuTemplate")
    bagDropdown:SetPoint("LEFT", bagLabel, "RIGHT", -4, -3)

    local bagOptions = {
        {text = "Bag Slot 1 (rightmost)", value = 1},
        {text = "Bag Slot 2", value = 2},
        {text = "Bag Slot 3", value = 3},
        {text = "Bag Slot 4 (leftmost)", value = 4},
        {text = "Backpack", value = 5},
    }

    local function BagDropdown_Initialize(self, level)
        for _, opt in ipairs(bagOptions) do
            local info = UIDropDownMenu_CreateInfo()
            info.text = opt.text
            info.value = opt.value
            info.func = function(btn)
                db.gearBagIndex = btn.value
                UIDropDownMenu_SetSelectedValue(bagDropdown, btn.value)
                UIDropDownMenu_SetText(bagDropdown, opt.text)
                Print("Gear bag set to: " .. GetBagName(db.gearBagIndex))
            end
            info.checked = (db.gearBagIndex == opt.value)
            UIDropDownMenu_AddButton(info, level)
        end
    end

    UIDropDownMenu_SetWidth(bagDropdown, 180)
    UIDropDownMenu_Initialize(bagDropdown, BagDropdown_Initialize)
    UIDropDownMenu_SetSelectedValue(bagDropdown, db.gearBagIndex)

    yOffset = yOffset - 44

    local orderLabel = panel:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    orderLabel:SetPoint("TOPLEFT", panel, "TOPLEFT", 16, yOffset)
    orderLabel:SetText("Sort Order:")

    local orderDropdown = CreateFrame("Frame", "GBSOrderDropdown", panel, "UIDropDownMenuTemplate")
    orderDropdown:SetPoint("LEFT", orderLabel, "RIGHT", -4, -3)

    local orderOptions = {
        {text = "Highest item level first", value = true},
        {text = "Lowest item level first", value = false},
    }

    local function OrderDropdown_Initialize(self, level)
        for _, opt in ipairs(orderOptions) do
            local info = UIDropDownMenu_CreateInfo()
            info.text = opt.text
            info.value = opt.value
            info.func = function(btn)
                db.sortDescending = btn.value
                UIDropDownMenu_SetSelectedValue(orderDropdown, btn.value)
                UIDropDownMenu_SetText(orderDropdown, opt.text)
                Print("Sort order: " .. opt.text)
            end
            info.checked = (db.sortDescending == opt.value)
            UIDropDownMenu_AddButton(info, level)
        end
    end

    UIDropDownMenu_SetWidth(orderDropdown, 200)
    UIDropDownMenu_Initialize(orderDropdown, OrderDropdown_Initialize)
    UIDropDownMenu_SetSelectedValue(orderDropdown, db.sortDescending)

    yOffset = yOffset - 50

    local autoSortCheck = CreateFrame("CheckButton", "GBSAutoSortCheck", panel, "InterfaceOptionsCheckButtonTemplate")
    autoSortCheck:SetPoint("TOPLEFT", panel, "TOPLEFT", 14, yOffset)
    autoSortCheck.Text:SetText(" Auto-sort gear bag when you click the Blizzard bag sort button")
    autoSortCheck:SetScript("OnClick", function(self)
        db.autoSort = self:GetChecked()
        Print("Auto-sort: " .. (db.autoSort and "|cff00ff00ON|r" or "|cffff0000OFF|r"))
    end)

    yOffset = yOffset - 32

    local debugCheck = CreateFrame("CheckButton", "GBSDebugCheck", panel, "InterfaceOptionsCheckButtonTemplate")
    debugCheck:SetPoint("TOPLEFT", panel, "TOPLEFT", 14, yOffset)
    debugCheck.Text:SetText(" Show debug messages in chat")
    debugCheck:SetScript("OnClick", function(self)
        db.debug = self:GetChecked()
    end)

    yOffset = yOffset - 44

    local sortBtn = CreateFrame("Button", nil, panel, "UIPanelButtonTemplate")
    sortBtn:SetPoint("TOPLEFT", panel, "TOPLEFT", 16, yOffset)
    sortBtn:SetSize(160, 28)
    sortBtn:SetText("Sort Gear Bag Now")
    sortBtn:SetScript("OnClick", function()
        SortGearBag()
    end)

    yOffset = yOffset - 48

    local helpHeader = panel:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    helpHeader:SetPoint("TOPLEFT", panel, "TOPLEFT", 16, yOffset)
    helpHeader:SetText("Slash Commands:")

    local helpText = panel:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    helpText:SetPoint("TOPLEFT", helpHeader, "BOTTOMLEFT", 0, -6)
    helpText:SetJustifyH("LEFT")
    helpText:SetText(
        "|cff888888/gbs sort|r - Sort gear bag now\n" ..
        "|cff888888/gbs bag <1-5>|r - Set gear bag\n" ..
        "|cff888888/gbs order <asc|desc>|r - Sort direction\n" ..
        "|cff888888/gbs auto|r - Toggle auto-sort\n" ..
        "|cff888888/gbs options|r - Open this panel\n" ..
        "|cff888888/gbs dump|r - Show bag contents with ilvl"
    )

    panel:SetScript("OnShow", function()
        autoSortCheck:SetChecked(db.autoSort)
        debugCheck:SetChecked(db.debug)
        UIDropDownMenu_SetSelectedValue(bagDropdown, db.gearBagIndex)
        UIDropDownMenu_SetSelectedValue(orderDropdown, db.sortDescending)
        for _, opt in ipairs(bagOptions) do
            if opt.value == db.gearBagIndex then
                UIDropDownMenu_SetText(bagDropdown, opt.text)
                break
            end
        end
        UIDropDownMenu_SetText(orderDropdown, db.sortDescending and "Highest item level first" or "Lowest item level first")
    end)

    return panel
end

---------------------------------------------------------------------------
-- Slash commands
---------------------------------------------------------------------------

SLASH_GEARBAGESORT1 = "/gbs"
SLASH_GEARBAGESORT2 = "/gearbagesort"
SlashCmdList["GEARBAGESORT"] = function(msg)
    local args = {}
    for word in msg:gmatch("%S+") do
        table.insert(args, word:lower())
    end

    local cmd = args[1]

    if cmd == "sort" then
        SortGearBag()
    elseif cmd == "bag" then
        local bagNum = tonumber(args[2])
        if bagNum and bagNum >= 1 and bagNum <= 5 then
            db.gearBagIndex = bagNum
            Print("Gear bag set to: " .. GetBagName(bagNum))
        else
            Print("Usage: /gbs bag <1-5> (1-4=bag slots, 5=backpack)")
        end
    elseif cmd == "order" then
        local order = args[2]
        if order == "asc" then
            db.sortDescending = false
            Print("Sort order: lowest ilvl first")
        elseif order == "desc" then
            db.sortDescending = true
            Print("Sort order: highest ilvl first")
        else
            Print("Usage: /gbs order <asc|desc>")
        end
    elseif cmd == "auto" then
        db.autoSort = not db.autoSort
        Print("Auto-sort: " .. (db.autoSort and "|cff00ff00ON|r" or "|cffff0000OFF|r"))
    elseif cmd == "debug" then
        db.debug = not db.debug
        Print("Debug: " .. (db.debug and "|cff00ff00ON|r" or "|cffff0000OFF|r"))
    elseif cmd == "options" or cmd == "config" or cmd == "settings" then
        Settings.OpenToCategory(ADDON_NAME)
    elseif cmd == "status" then
        Print("Gear bag: " .. GetBagName(db.gearBagIndex) .. " (slot " .. db.gearBagIndex .. ")")
        Print("Sort order: " .. (db.sortDescending and "highest first" or "lowest first"))
        Print("Auto-sort: " .. (db.autoSort and "|cff00ff00ON|r" or "|cffff0000OFF|r"))
    elseif cmd == "dump" then
        local bagID = UserBagToGameBag(db.gearBagIndex)
        local numSlots = C_Container.GetContainerNumSlots(bagID)
        Print("Contents of " .. GetBagName(db.gearBagIndex) .. ":")
        for slot = 1, numSlots do
            local item = ReadSlot(bagID, slot)
            if not item.isEmpty then
                Print(string.format("  Slot %d: ilvl |cffffffff%d|r q%d - %s",
                    slot, item.ilvl, item.quality, item.itemLink or "?"))
            else
                Print(string.format("  Slot %d: |cff888888empty|r", slot))
            end
        end
    else
        Print("/gbs sort - Sort gear bag now")
        Print("/gbs bag <1-5> - Set gear bag")
        Print("/gbs order <asc|desc> - Sort direction")
        Print("/gbs auto - Toggle auto-sort")
        Print("/gbs options - Open settings panel")
        Print("/gbs status - Show settings")
        Print("/gbs dump - Show bag contents with ilvl")
        Print("/gbs debug - Toggle debug output")
    end
end

---------------------------------------------------------------------------
-- Events
---------------------------------------------------------------------------

local eventFrame = CreateFrame("Frame")
eventFrame:RegisterEvent("ADDON_LOADED")
eventFrame:RegisterEvent("BAG_UPDATE_DELAYED")
eventFrame:RegisterEvent("PLAYER_LOGOUT")

eventFrame:SetScript("OnEvent", function(self, event, ...)
    if event == "ADDON_LOADED" then
        local addonName = ...
        if addonName == ADDON_NAME then
            if not GearBagSortDB then
                GearBagSortDB = {}
            end
            db = GearBagSortDB
            for k, v in pairs(defaults) do
                if db[k] == nil then
                    db[k] = v
                end
            end

            local optionsPanel = CreateOptionsPanel()
            if Settings and Settings.RegisterCanvasLayoutCategory then
                local category = Settings.RegisterCanvasLayoutCategory(optionsPanel, ADDON_NAME)
                category.ID = ADDON_NAME
                Settings.RegisterAddOnCategory(category)
            end

            Print("v1.3 loaded! |cff888888/gbs sort|r or |cff888888/gbs options|r")
        end

    elseif event == "BAG_UPDATE_DELAYED" then
        OnBagSortFinished()

    elseif event == "PLAYER_LOGOUT" then
        GearBagSortDB = db
    end
end)
