--------------------------------------------------------------------------------
-- S-Frames: Bag Sorting Logic (Bags/Sort.lua)
-- Manual sorting algorithm compatible with Vanilla/Turtle WoW
--------------------------------------------------------------------------------

SFrames.Bags.Sort = SFrames.Bags.Sort or {}

local isSorting = false
local sortQueue = {}
local sortTimer = CreateFrame("Frame")
local sortDelay = 0.05 -- Increased speed from 0.15 to accelerate sorting.
local timeSinceLast = 0
local activeCompleteMessage = nil
local activeCompleteUpdate = nil
local activeBagOrder = nil
local activePhase = nil
local TEXT_LOCKED = "\233\131\168\229\136\134\231\137\169\229\147\129\229\183\178\233\148\129\229\174\154\239\188\140\232\175\183\231\168\141\229\144\142\233\135\141\232\175\149\227\128\130"
local TEXT_OFFLINE_BAGS = "\231\166\187\231\186\191\230\168\161\229\188\143\228\184\139\230\151\160\230\179\149\230\149\180\231\144\134\229\156\168\231\186\191\232\131\140\229\140\133\227\128\130"
local TEXT_BAG_DONE = "\232\131\140\229\140\133\230\149\180\231\144\134\229\174\140\230\136\144\227\128\130"
local TEXT_OFFLINE_BANK = "\231\166\187\231\186\191\230\168\161\229\188\143\228\184\139\230\151\160\230\179\149\230\149\180\231\144\134\229\156\168\231\186\191\233\147\182\232\161\140\227\128\130"
local TEXT_BANK_DONE = "\233\147\182\232\161\140\230\149\180\231\144\134\229\174\140\230\136\144\227\128\130"
local itemMaxStackCache = {}

local function GetItemSortValue(link)
    if not link then return 0, "" end

    local _, _, itemString = string.find(link, "^|c%x+|H(.+)|h%[.*%]")
    if not itemString then itemString = link end

    local itemName, _, itemRarity, _, itemType = GetItemInfo(itemString)
    if not itemName then return 0, "" end

    local score = 0
    local _, _, itemID = string.find(link, "item:(%d+)")
    if itemID == "6948" or string.find(itemName, "Hearthstone") then
        score = 1000000
    else
        score = score + (itemRarity or 0) * 100000

        if itemType == "Weapon" then
            score = score + 50000
        elseif itemType == "Armor" then
            score = score + 40000
        elseif itemType == "Consumable" then
            score = score + 30000
        elseif itemType == "Trade Goods" then
            score = score + 20000
        elseif itemType == "Recipe" then
            score = score + 10000
        end
    end

    return score, itemName
end

local function GetItemIdentity(link)
    if not link then return nil end
    local _, _, itemString = string.find(link, "|H([^|]+)|h")
    if not itemString then itemString = link end
    return itemString
end

local function GetItemMaxStack(link)
    local itemKey = GetItemIdentity(link)
    if not itemKey then return 1 end

    if itemMaxStackCache[itemKey] then
        return itemMaxStackCache[itemKey]
    end

    local maxStack = 1
    local res = {GetItemInfo(itemKey)}
    -- In standard vanilla, maxStack is parameter 7. In some modified schema (TBC) it is parameter 8.
    local v7 = tonumber(res[7])
    local v8 = tonumber(res[8])
    if v7 and v7 > 1 then
        maxStack = v7
    elseif v8 and v8 > 1 then
        maxStack = v8
    end

    itemMaxStackCache[itemKey] = maxStack
    return maxStack
end

local function GetSpecialType(link, isBag)
    if not link then return "Normal" end
    local _, _, itemString = string.find(link, "^|c%x+|H(.+)|h%[.*%]")
    if not itemString then itemString = link end
    local itemName, _, _, _, itemType, itemSubType = GetItemInfo(itemString)
    
    if not itemType and not itemName then return "Normal" end
    
    local _, _, itemID = string.find(itemString, "item:(%d+)")

    if isBag then
        if itemType == "Quiver" or itemType == "箭袋" then return "Ammo" end
        if itemSubType == "Quiver" or itemSubType == "Ammo Pouch" or itemSubType == "箭袋" or itemSubType == "子弹袋" then return "Ammo" end
        if itemSubType == "Soul Bag" or itemSubType == "灵魂袋" then return "Soul" end
        if itemSubType == "Enchanting Bag" or itemSubType == "附魔材料袋" then return "Enchanting" end
        if itemSubType == "Herb Bag" or itemSubType == "草药袋" then return "Herb" end
    else
        if itemType == "Projectile" or itemType == "弹药" then return "Ammo" end
        if itemSubType == "Arrow" or itemSubType == "Bullet" or itemSubType == "箭" or itemSubType == "子弹" then return "Ammo" end
        if itemID == "6265" or itemName == "Soul Shard" or itemName == "灵魂碎片" then return "Soul" end
    end
    
    return "Normal"
end

local function SortItemsByRule(items)
    table.sort(items, function(a, b)
        if a.score ~= b.score then
            return a.score > b.score
        elseif a.name ~= b.name then
            return a.name < b.name
        else
            return a.count > b.count
        end
    end)
end

local function ResetSortState()
    isSorting = false
    sortQueue = {}
    activeCompleteMessage = nil
    activeCompleteUpdate = nil
    activeBagOrder = nil
    activePhase = nil
    timeSinceLast = 0
    sortTimer:Hide()
end

local function FinishSort()
    local msg = activeCompleteMessage
    local updater = activeCompleteUpdate
    ResetSortState()

    if msg then SFrames:Print(msg) end
    if updater then updater() end
end

function SFrames.Bags.Sort:ExecuteSimpleSort(items, bagOrder)
    sortQueue = {}
    
    local slotPool = { Normal = {}, Ammo = {}, Soul = {}, Enchanting = {}, Herb = {} }
    local allocatedStream = {}
    
    for _, bag in ipairs(bagOrder) do
        local btype = "Normal"
        if bag ~= 0 and bag ~= -1 then
            local invID
            if bag >= 1 and bag <= 4 then 
                invID = ContainerIDToInventoryID(bag)
            elseif bag >= 5 and bag <= 11 then 
                invID = bag + 34 
            end
            if invID then
                local ok, link = pcall(GetInventoryItemLink, "player", invID)
                if ok and link then btype = GetSpecialType(link, true) end
            end
        end
        
        local bagSlots = GetContainerNumSlots(bag) or 0
        for slot = 1, bagSlots do
            local s = { bag = bag, slot = slot, type = btype }
            table.insert(allocatedStream, s)
            if slotPool[btype] then
                table.insert(slotPool[btype], s)
            else
                table.insert(slotPool.Normal, s)
            end
        end
    end
    
    for _, item in ipairs(items) do
        item.specialType = GetSpecialType(item.link, false)
    end
    
    -- Now assign target slot to each item
    for _, item in ipairs(items) do
        local targetSlot = nil
        
        if item.specialType ~= "Normal" and slotPool[item.specialType] and table.getn(slotPool[item.specialType]) > 0 then
            targetSlot = table.remove(slotPool[item.specialType], 1)
        elseif table.getn(slotPool.Normal) > 0 then
            targetSlot = table.remove(slotPool.Normal, 1)
        end
        
        if targetSlot then
            targetSlot.idealItem = item
        end
    end
    
    -- Build virtual moves to align physical items to targetSlots
    for _, target in ipairs(allocatedStream) do
        if target.idealItem then
            local idealItem = target.idealItem
            if idealItem.bag ~= target.bag or idealItem.slot ~= target.slot then
                table.insert(sortQueue, {
                    fromBag = idealItem.bag,
                    fromSlot = idealItem.slot,
                    toBag = target.bag,
                    toSlot = target.slot,
                    retries = 0
                })
                
                -- Reflect the virtual swap in our model so later moves stay correct.
                for j = 1, table.getn(items) do
                    if items[j].bag == target.bag and items[j].slot == target.slot then
                        items[j].bag = idealItem.bag
                        items[j].slot = idealItem.slot
                        break
                    end
                end
                
                idealItem.bag = target.bag
                idealItem.slot = target.slot
            end
        end
    end
end

function SFrames.Bags.Sort:ScanItems(bagOrder, ignoreLocked)
    local items = {}

    for _, bag in ipairs(bagOrder) do
        local bagSlots = GetContainerNumSlots(bag) or 0
        for slot = 1, bagSlots do
            local link = GetContainerItemLink(bag, slot)
            local _, itemCount, locked = GetContainerItemInfo(bag, slot)

            if locked and not ignoreLocked then
                return nil, TEXT_LOCKED
            end

            if link then
                local score, name = GetItemSortValue(link)
                table.insert(items, {
                    bag = bag,
                    slot = slot,
                    link = link,
                    score = score,
                    name = name,
                    count = itemCount or 1,
                    stackKey = GetItemIdentity(link),
                    maxStack = GetItemMaxStack(link)
                })
            end
        end
    end

    return items, nil
end

function SFrames.Bags.Sort:BuildStackMergeMoves(items)
    local grouped = {}
    local moves = {}

    for _, item in ipairs(items) do
        if item and item.stackKey and item.maxStack and item.maxStack > 1 and (item.count or 0) > 0 then
            if not grouped[item.stackKey] then
                grouped[item.stackKey] = {
                    maxStack = item.maxStack,
                    slots = {}
                }
            end

            if item.maxStack > grouped[item.stackKey].maxStack then
                grouped[item.stackKey].maxStack = item.maxStack
            end

            table.insert(grouped[item.stackKey].slots, {
                bag = item.bag,
                slot = item.slot,
                count = item.count
            })
        end
    end

    for _, group in pairs(grouped) do
        local slots = group.slots
        if table.getn(slots) > 1 then
            table.sort(slots, function(a, b)
                if a.count ~= b.count then
                    return a.count > b.count
                elseif a.bag ~= b.bag then
                    return a.bag < b.bag
                else
                    return a.slot < b.slot
                end
            end)

            local left = 1
            local right = table.getn(slots)
            while left < right do
                local target = slots[left]
                local source = slots[right]

                if target.count >= group.maxStack then
                    left = left + 1
                elseif source.count <= 0 then
                    right = right - 1
                else
                    local transfer = math.min(group.maxStack - target.count, source.count)
                    if transfer > 0 then
                        table.insert(moves, {
                            fromBag = source.bag,
                            fromSlot = source.slot,
                            toBag = target.bag,
                            toSlot = target.slot,
                            transferCount = transfer,
                            isStackMove = true
                        })
                        target.count = target.count + transfer
                        source.count = source.count - transfer
                    end

                    if source.count <= 0 then
                        right = right - 1
                    end
                    if target.count >= group.maxStack then
                        left = left + 1
                    end
                end
            end
        end
    end

    return moves
end

function SFrames.Bags.Sort:StartPlacementPhase(ignoreLocked)
    if not activeBagOrder then
        FinishSort()
        return
    end

    local items, err = self:ScanItems(activeBagOrder, ignoreLocked)
    if not items then
        ResetSortState()
        if err then SFrames:Print(err) end
        return
    end

    SortItemsByRule(items)
    self:ExecuteSimpleSort(items, activeBagOrder)

    if table.getn(sortQueue) == 0 then
        FinishSort()
        return
    end

    activePhase = "sort"
    sortTimer:Show()
end

function SFrames.Bags.Sort:StartForBags(bagOrder, completeMessage, completeUpdate)
    if isSorting then return end

    isSorting = true
    sortQueue = {}
    timeSinceLast = 0
    activeCompleteMessage = completeMessage
    activeCompleteUpdate = completeUpdate
    activeBagOrder = bagOrder
    activePhase = nil

    local items, err = self:ScanItems(bagOrder, false)
    if not items then
        ResetSortState()
        if err then SFrames:Print(err) end
        return
    end

    local stackMoves = self:BuildStackMergeMoves(items)
    if table.getn(stackMoves) > 0 then
        sortQueue = stackMoves
        activePhase = "stack"
        sortTimer:Show()
        return
    end

    self:StartPlacementPhase(false)
end

function SFrames.Bags.Sort:Start()
    if SFrames.Bags.Container and SFrames.Bags.Container.isOffline then
        SFrames:Print(TEXT_OFFLINE_BAGS)
        return
    end

    self:StartForBags(
        {0, 1, 2, 3, 4},
        TEXT_BAG_DONE,
        function()
            if SFrames.Bags.Container then
                SFrames.Bags.Container:UpdateLayout()
            end
        end
    )
end

function SFrames.Bags.Sort:StartBank()
    if SFrames.Bags.Bank and SFrames.Bags.Bank.isOffline then
        SFrames:Print(TEXT_OFFLINE_BANK)
        return
    end

    self:StartForBags(
        {-1, 5, 6, 7, 8, 9, 10, 11},
        TEXT_BANK_DONE,
        function()
            if SFrames.Bags.Bank then
                SFrames.Bags.Bank:UpdateLayout()
            end
        end
    )
end

sortTimer:SetScript("OnUpdate", function()
    if not isSorting then
        this:Hide()
        return
    end

    timeSinceLast = timeSinceLast + arg1
    if timeSinceLast > sortDelay then
        timeSinceLast = 0

        if table.getn(sortQueue) > 0 then
            local move = sortQueue[1]
            local _, _, lockedFrom = GetContainerItemInfo(move.fromBag, move.fromSlot)
            local _, _, lockedTo = GetContainerItemInfo(move.toBag, move.toSlot)

            if lockedFrom or lockedTo or CursorHasItem() then
                move.retries = (move.retries or 0) + 1
                if move.retries > 40 then
                    table.remove(sortQueue, 1)
                    if CursorHasItem() then
                        PickupContainerItem(move.fromBag, move.fromSlot)
                    end
                end
                return
            end

            table.remove(sortQueue, 1)
            if move.isStackMove and move.transferCount then
                SplitContainerItem(move.fromBag, move.fromSlot, move.transferCount)
                PickupContainerItem(move.toBag, move.toSlot)
            else
                PickupContainerItem(move.fromBag, move.fromSlot)
                PickupContainerItem(move.toBag, move.toSlot)
                if CursorHasItem() then
                    PickupContainerItem(move.fromBag, move.fromSlot)
                    if CursorHasItem() then
                        PickupContainerItem(move.toBag, move.toSlot)
                    end
                end
            end
        else
            if activePhase == "stack" then
                if activeBagOrder then
                    for _, bag in ipairs(activeBagOrder) do
                        local bagSlots = GetContainerNumSlots(bag) or 0
                        for slot = 1, bagSlots do
                            local _, _, locked = GetContainerItemInfo(bag, slot)
                            if locked then
                                return
                            end
                        end
                    end
                end
                -- After consolidation, rescan inventory and run normal sorting.
                SFrames.Bags.Sort:StartPlacementPhase(true)
            else
                if CursorHasItem() then return end
                FinishSort()
            end
        end
    end
end)
sortTimer:Hide()
