-- /*=============================================
-- =         Multi-Action state globals         =
-- =============================================*/
multiPanel = nil
cacheMultiActionButtons = {}
multiActionCooldownEvents = {}

local function splitButtonId(button)
    return string.match(button:getId(), "(.*)%.(.*)")
end

local function countFilledMultiSlots(multiActions)
    if not multiActions then
        return 0
    end
    local count = 0
    for i = 1, 3 do
        if type(multiActions[i]) == "table" and next(multiActions[i]) ~= nil then
            count = count + 1
        end
    end
    return count
end

local function localGetActionName(actionType)
    if type(actionType) == "string" then
        return actionType
    end
    return getActionName(actionType)
end

local function clearSingleActionCache(button, barID, buttonID)
    button.cache.param = ""
    button.cache.sendAutomatic = false
    button.cache.itemId = 0
    button.cache.actionType = 0
    button.cache.upgradeTier = 0
    button.cache.smartMode = false

    local entry = ApiJson.getMapping(tonumber(barID), tonumber(buttonID))
    local actionsetting = entry and entry["actionsetting"]
    if actionsetting then
        actionsetting["chatText"] = nil
        actionsetting["sendAutomatically"] = nil
        actionsetting["useObject"] = nil
        actionsetting["useType"] = nil
        actionsetting["upgradeTier"] = nil
        actionsetting["useEquipSmartMode"] = nil
    end
end

local function playerCanUseSpellLocal(spellData)
    if not g_game.isOnline() or not spellData then
        return false
    end
    if spellData.needLearn and not spellListData[tostring(spellData.id)] then
        return false
    end
    if spellData.mana and player and player:getMana() < spellData.mana then
        return false
    end
    if spellData.level and player and player:getLevel() < spellData.level then
        return false
    end
    if spellData.soul and player and player:getSoul() < spellData.soul then
        return false
    end
    if spellData.vocations and player and
        not table.contains(spellData.vocations, translateVocation(player:getVocation())) then
        return false
    end
    return true
end

local function hasActiveSpellCooldown(spellId)
    local cooldownData = spellCooldownCache[spellId]
    if cooldownData and (cooldownData.startTime + cooldownData.exhaustion) > g_clock.millis() then
        return true
    end
    return false
end

local function getSpellCooldownRemaining(spellId)
    local cooldownData = spellCooldownCache[spellId]
    if not cooldownData then
        return 0
    end
    local remaining = (cooldownData.startTime + cooldownData.exhaustion) - g_clock.millis()
    return remaining > 0 and remaining or 0
end

local function getSpellGroupCooldownRemaining(spellData)
    if not spellData or not spellData.group or not spellGroupCooldownCache then
        return 0
    end

    local groupIds = spellData._cachedGroupIds or Spells.getGroupIds(spellData)
    if not groupIds then
        return 0
    end

    local maxRemaining = 0
    local now = g_clock.millis()
    for _, groupId in pairs(groupIds) do
        local groupCooldown = spellGroupCooldownCache[groupId]
        if groupCooldown then
            local remaining = (groupCooldown.startTime + groupCooldown.exhaustion) - now
            if remaining > maxRemaining then
                maxRemaining = remaining
            end
        end
    end
    return maxRemaining > 0 and maxRemaining or 0
end

local function applySpellCooldownsToButton(button, spellData)
    if not button or not spellData then
        return
    end

    checkRemainSpellCooldown(button, spellData.id)
    if not hasActiveSpellCooldown(spellData.id) then
        local groupCooldownRemaining = getSpellGroupCooldownRemaining(spellData)
        if groupCooldownRemaining > 0 then
            updateCooldown(button, groupCooldownRemaining)
            if button.cache.removeCooldownEvent then
                removeEvent(button.cache.removeCooldownEvent)
            end
            button.cache.removeCooldownEvent = scheduleEvent(function()
                removeCooldown(button)
            end, groupCooldownRemaining)
        end
    end
end

-- /*=============================================
-- =           Rotation engine                   =
-- =============================================*/
local function findNextAvailableAction(multiActions)
    if not multiActions or table.empty(multiActions) then
        return nil
    end

    local bestAction = nil
    local closestCooldownAction = nil
    local closestCooldownTime = math.huge
    local firstValidAction = nil
    local onlyOneSpell = true
    local firstSpellData = nil
    local spellCount = 0

    for i, data in ipairs(multiActions) do
        repeat
        if not data or table.empty(data) then
            break
        end

        if data["chatText"] then
            local spellData = Spells.getSpellDataByParamWords(data["chatText"]:lower())
            if spellData then
                spellCount = spellCount + 1
                if not firstSpellData then
                    firstSpellData = spellData
                elseif firstSpellData.id ~= spellData.id then
                    onlyOneSpell = false
                end

                local canUse = playerCanUseSpellLocal(spellData)
                if not canUse and onlyOneSpell and spellCount == 1 then
                    firstValidAction = firstValidAction or data
                    break
                end

                if not canUse then
                    break
                end

                firstValidAction = firstValidAction or data
                local spellCooldownRemaining = getSpellCooldownRemaining(spellData.id)
                local groupCooldownRemaining = getSpellGroupCooldownRemaining(spellData)
                local totalCooldownRemaining = math.max(spellCooldownRemaining, groupCooldownRemaining)

                if totalCooldownRemaining <= 0 then
                    bestAction = bestAction or data
                elseif totalCooldownRemaining < closestCooldownTime then
                    closestCooldownTime = totalCooldownRemaining
                    closestCooldownAction = data
                end
            else
                firstValidAction = firstValidAction or data
                bestAction = bestAction or data
            end
        elseif data["useObject"] then
            local itemId = data["useObject"]
            local upgradeTier = data["upgradeTier"] or 0
            local itemCount = player and player:getInventoryCount(itemId, upgradeTier) or 0
            firstValidAction = firstValidAction or data

            local runeSpellData = Spells.getRuneSpellByItem(itemId)
            if runeSpellData then
                local spellCooldownRemaining = getSpellCooldownRemaining(runeSpellData.id)
                local groupCooldownRemaining = getSpellGroupCooldownRemaining(runeSpellData)
                local totalCooldownRemaining = math.max(spellCooldownRemaining, groupCooldownRemaining)

                if totalCooldownRemaining <= 0 and itemCount > 0 then
                    bestAction = bestAction or data
                elseif itemCount > 0 and totalCooldownRemaining < closestCooldownTime then
                    closestCooldownTime = totalCooldownRemaining
                    closestCooldownAction = data
                end
            elseif itemCount > 0 then
                bestAction = bestAction or data
            end
        end

        until true
    end

    return bestAction or closestCooldownAction or firstValidAction
end

-- /*=============================================
-- =    Shared slot rendering helper             =
-- =============================================*/
local function renderSlotOnWidget(widget, slotData, isMainButton)
    if not widget or not slotData or table.empty(slotData) then
        return
    end

    if slotData["useObject"] then
        if isMainButton then
            widget.cache.isSpell = false
            widget.cache.isRuneSpell = false
            widget.cache.spellID = 0
            widget.cache.spellData = nil
            widget.cache.primaryGroup = nil
            widget.item.text:setImageSource("")
            widget.item.text:setText("")
        end

        widget.item:setItemId(slotData["useObject"], true)
        widget.item:setOn(true)
        widget.cache.itemId = slotData["useObject"]
        widget.cache.upgradeTier = slotData["upgradeTier"] or 0
        widget.cache.smartMode = slotData["useEquipSmartMode"] or false
        local useTypeName = localGetActionName(slotData["useType"]) or "Use"
        widget.cache.actionType = UseTypes[useTypeName] or UseTypes["Use"]

        local itemCount = player and player:getInventoryCount(widget.cache.itemId, widget.cache.upgradeTier) or 0
        widget.item:setItemCount(itemCount)
        if widget.item.text and widget.item.text.gray then
            widget.item.text.gray:setVisible(itemCount == 0)
        end
        if widget.cache.actionType == UseTypes["Equip"] then
            local equipped = player and player:hasEquippedItemId(widget.cache.itemId, widget.cache.upgradeTier)
            widget.item:setChecked(itemCount ~= 0 and equipped)
        end

        local runeSpellData = Spells.getRuneSpellByItem(widget.cache.itemId)
        if runeSpellData then
            widget.cache.isRuneSpell = true
            widget.cache.spellData = runeSpellData
            local groupIds = Spells.getGroupIds(runeSpellData)
            widget.cache.primaryGroup = groupIds and groupIds[1] or nil
            applySpellCooldownsToButton(widget, runeSpellData)
        end
    elseif slotData["chatText"] then
        local spellData, param = Spells.getSpellDataByParamWords(slotData["chatText"]:lower())
        if spellData then
            local spellId = spellData.clientId
            if spellId then
                local source = SpelllistSettings['Default'].iconFile
                local clip = Spells.getImageClip(spellId, 'Default')
                widget.item.text:setText("")
                widget.item.text:setImageSource(source)
                widget.item.text:setImageClip(clip)
            end
            widget.cache.isSpell = true
            widget.cache.spellID = spellData.id
            widget.cache.spellData = spellData
            local groupIds = Spells.getGroupIds(spellData)
            widget.cache.primaryGroup = groupIds and groupIds[1] or nil

            if param then
                local formatedParam = param:gsub('"', '')
                widget.parameterText:setText(short_text('"' .. formatedParam, 4))
                widget.cache.castParam = formatedParam
            elseif isMainButton then
                widget.parameterText:setText("")
                widget.cache.castParam = nil
            end

            if widget.item.text and widget.item.text.gray then
                widget.item.text.gray:setVisible(not playerCanUseSpellLocal(spellData))
            end

            widget.cache.isRuneSpell = false
            applySpellCooldownsToButton(widget, spellData)
        else
            if isMainButton then
                widget.cache.isSpell = false
                widget.cache.isRuneSpell = false
                widget.cache.spellID = 0
                widget.cache.spellData = nil
                widget.cache.primaryGroup = nil
                widget.item.text:setImageSource("")
            end
            widget.item.text:setText(short_text(slotData["chatText"], 15))
        end
        widget.item:setOn(true)
        widget.cache.param = slotData["chatText"]
        widget.cache.sendAutomatic = slotData["sendAutomatically"]
        widget.cache.actionType = UseTypes["chatText"]
    end

    setupButtonTooltip(widget, false)
end

function updateMultiButtonState(button)
    if not button or not button.item or not player or not button.cache then
        return
    end
    if not button.cache.multiActions or table.empty(button.cache.multiActions) then
        if updateButton then
            updateButton(button)
        end
        return
    end

    local action = findNextAvailableAction(button.cache.multiActions)
    if not action then
        action = button.cache.multiActions[1]
    end
    if not action or table.empty(action) then
        return
    end

    -- Early return: already displaying this chatText action
    if action["chatText"] and button.cache.param == action["chatText"] and button.cache.sendAutomatic ==
        action["sendAutomatically"] and button.cache.actionType == UseTypes["chatText"] then
        return
    end

    -- Early return: already displaying this useObject action
    if action["useObject"] and button.cache.itemId == action["useObject"] then
        local useTypeName = localGetActionName(action["useType"]) or "Use"
        if button.cache.actionType == (UseTypes[useTypeName] or UseTypes["Use"]) then
            return
        end
    end

    removeCooldown(button)
    renderSlotOnWidget(button, action, true)

    if button.multiIcon then
        button.multiIcon:setVisible(countFilledMultiSlots(button.cache.multiActions) >= 2)
    end
    if cacheMultiActionButtons then
        cacheMultiActionButtons[button] = true
    end
end

-- /*=============================================
-- =         Cooldown event scheduling           =
-- =============================================*/
function scheduleMultiActionCooldownEvent(button, eventKey, delay)
    if not button or not eventKey or not delay then
        return
    end

    local buttonId = button:getId()
    if not multiActionCooldownEvents[buttonId] then
        multiActionCooldownEvents[buttonId] = {}
    end

    if multiActionCooldownEvents[buttonId][eventKey] then
        removeEvent(multiActionCooldownEvents[buttonId][eventKey])
    end

    local eventId = scheduleEvent(function()
        if button and not button:isDestroyed() then
            updateMultiButtonState(button)
        end
        if multiActionCooldownEvents[buttonId] then
            multiActionCooldownEvents[buttonId][eventKey] = nil
        end
    end, delay + 100)

    multiActionCooldownEvents[buttonId][eventKey] = eventId
end

function registerMultiActionCooldownEvents(button)
    if not button or not button.cache or not button.cache.multiActions or table.empty(button.cache.multiActions) then
        return
    end

    local buttonId = button:getId()
    if multiActionCooldownEvents[buttonId] then
        for _, eventId in pairs(multiActionCooldownEvents[buttonId]) do
            removeEvent(eventId)
        end
        multiActionCooldownEvents[buttonId] = nil
    end

    for _, actionData in pairs(button.cache.multiActions) do
        if actionData and actionData["chatText"] then
            local spellData = Spells.getSpellDataByParamWords(actionData["chatText"]:lower())
            if spellData then
                local remaining = math.max(getSpellCooldownRemaining(spellData.id),
                    getSpellGroupCooldownRemaining(spellData))
                if remaining > 0 then
                    local eventKey = "spell_" .. spellData.id
                    scheduleMultiActionCooldownEvent(button, eventKey, remaining)
                end
            end
        elseif actionData and actionData["useObject"] then
            local runeSpellData = Spells.getRuneSpellByItem(actionData["useObject"])
            if runeSpellData then
                local remaining = math.max(getSpellCooldownRemaining(runeSpellData.id),
                    getSpellGroupCooldownRemaining(runeSpellData))
                if remaining > 0 then
                    local eventKey = "rune_" .. runeSpellData.id
                    scheduleMultiActionCooldownEvent(button, eventKey, remaining)
                end
            end
        end
    end
end

function clearMultiActionCooldownEvents(buttonId)
    if not buttonId or not multiActionCooldownEvents[buttonId] then
        return
    end
    for _, eventId in pairs(multiActionCooldownEvents[buttonId]) do
        removeEvent(eventId)
    end
    multiActionCooldownEvents[buttonId] = nil
end

-- /*=============================================
-- =         Multi-action slot dialogs           =
-- =============================================*/
function assignMultiActionSpell(button, multiButtonIndex)
    assignSpell(button, multiButtonIndex)
end

function assignMultiText(button, multiButtonIndex)
    assignText(button, multiButtonIndex)
end

function assignMultiItem(button, multiButtonIndex, itemId, itemTier, dragEvent)
    assignItem(button, itemId, itemTier or 0, dragEvent, multiButtonIndex)
end

-- /*=============================================
-- =         Popup layout / position             =
-- =============================================*/
function getMultiActionLayout(barN)
    barN = tonumber(barN) or 1
    if barN >= 1 and barN <= 3 then
        return "BottomMultiAction"
    elseif barN >= 4 and barN <= 6 then
        return "LeftMultiAction"
    elseif barN >= 7 and barN <= 9 then
        return "RightMultiAction"
    end
    return "BottomMultiAction"
end

function getMultiActionPosition(button)
    local actionbar = button:getParent():getParent()
    local barN = actionbar and actionbar.n or 1

    if barN >= 1 and barN <= 3 then
        return topoint(string.format("%s %s", button:getX() - 29, button:getY() - 116))
    elseif barN >= 4 and barN <= 6 then
        return topoint(string.format("%s %s", button:getX() + 34, button:getY() - 29))
    elseif barN >= 7 and barN <= 9 then
        return topoint(string.format("%s %s", button:getX() - 116, button:getY() - 29))
    end
    return button:getPosition()
end

-- /*=============================================
-- =         Cleanup                             =
-- =============================================*/
function closeCurrentMultiActionPanel()
    if dragButton and dragItem then
        local reparentedDragItem = false
        if dragButton.multiButtonIndex and dragButton.parentButton then
            if dragItem and not dragItem:isDestroyed() then
                dragItem:setPhantom(false)
                dragItem:setParent(dragButton)
                dragItem:fill('parent')
                reparentedDragItem = true
            end
            if dragButton.cooldown then
                dragButton.cooldown:setBorderWidth(0)
            end
            if dragButton.cache then
                dragButton.cache.isDragging = false
            end
        end
        if not reparentedDragItem and dragItem and not dragItem:isDestroyed() then
            dragItem:destroy()
        end
        dragButton = nil
        dragItem = nil
    end

    if multiPanel then
        local refButton = multiPanel.button
        if refButton then
            refButton.onGeometryChange = nil
            refButton.onVisibilityChange = nil
            refButton.multiPanel = nil
        end
        if gameRootPanel then
            gameRootPanel.onMouseRelease = multiPanel.prevMouseReleaseHandler
        end
        if not multiPanel:isDestroyed() then
            multiPanel:destroy()
        end
        multiPanel = nil
    end
end

-- /*=============================================
-- =         Right-click menu inside panel       =
-- =============================================*/
function onMultiActionButtonMouseRelease(actionButton, mousePos, mouseButton, parentButton, data)
    if mouseButton ~= MouseRightButton then
        return
    end

    local multiButtonIndex = tonumber(string.match(actionButton:getId(), "actionButton(.*)"))
    local menu = g_ui.createWidget('PopupMenu')
    menu:setGameMenu(true)

    menu:addOption(actionButton.cache and actionButton.cache.isSpell and tr('Edit Spell') or tr('Assign Spell'),
        function()
            assignMultiActionSpell(parentButton, multiButtonIndex)
        end)

    if actionButton.item and actionButton.item:getItemId() > 100 then
        menu:addOption(tr('Edit Object'), function()
            assignMultiItem(parentButton, multiButtonIndex, actionButton.item:getItemId(), 0, false)
        end)
    else
        menu:addOption(tr('Assign Object'), function()
            assignItemEvent(parentButton, multiButtonIndex)
        end)
    end

    local hasText = actionButton.item and actionButton.item.text and actionButton.item.text:getText():len() > 0
    menu:addOption(hasText and tr('Edit Text') or tr('Assign Text'), function()
        assignMultiText(parentButton, multiButtonIndex)
    end)

    if data and not table.empty(data) then
        menu:addSeparator()
        menu:addOption(tr("Clear Action"), function()
            if parentButton.cache.multiActions and parentButton.cache.multiActions[multiButtonIndex] then
                local barID, buttonID = splitButtonId(parentButton)
                ApiJson.removeMultiAction(tonumber(barID), tonumber(buttonID), multiButtonIndex)
                parentButton.cache.multiActions[multiButtonIndex] = {}

                if not hasMultiActions(parentButton.cache.multiActions) then
                    parentButton.cache.multiActions = {{}, {}, {}}
                    if parentButton.multiIcon then
                        parentButton.multiIcon:setVisible(false)
                    end
                    cacheMultiActionButtons[parentButton] = nil
                    clearMultiActionCooldownEvents(parentButton:getId())
                    closeCurrentMultiActionPanel()
                    clearButton(parentButton, false)
                else
                    assignMultiAction(parentButton, true)
                    updateMultiButtonState(parentButton)
                end
            end
        end)
    end
    menu:display(mousePos)
end

-- /*=============================================
-- =         Popup creation / refresh            =
-- =============================================*/
local function resetMultiPanelButton(slotButton)
    if not slotButton then
        return
    end
    if slotButton.item then
        if slotButton.item:getItemId() ~= 0 then
            slotButton.item:setItemId(0)
        end
        if slotButton.item:isOn() then
            slotButton.item:setOn(false)
        end
        if slotButton.item:isChecked() then
            slotButton.item:setChecked(false)
        end
        if slotButton.item.text then
            slotButton.item.text:setImageSource("")
            slotButton.item.text:setText("")
            if slotButton.item.text.gray then
                slotButton.item.text.gray:setVisible(false)
            end
        end
        slotButton.item:setDraggable(true)
    end
    if slotButton.parameterText then
        slotButton.parameterText:setText("")
    end
    slotButton.cache = {}
    setupButtonTooltip(slotButton, true)
end

function assignMultiAction(button, skipPrefill)
    if not button then
        return
    end

    local actionbar = button:getParent() and button:getParent():getParent()
    local barN = actionbar and actionbar.n or 1

    if not multiPanel or multiPanel:isDestroyed() or multiPanel.button ~= button then
        if multiPanel and not multiPanel:isDestroyed() then
            if multiPanel.button then
                multiPanel.button.onGeometryChange = nil
                multiPanel.button.onVisibilityChange = nil
                multiPanel.button.multiPanel = nil
            end
            if gameRootPanel then
                gameRootPanel.onMouseRelease = multiPanel.prevMouseReleaseHandler
            end
            multiPanel:destroy()
        end

        multiPanel = g_ui.createWidget(getMultiActionLayout(barN), gameRootPanel)
        button.multiPanel = multiPanel
        multiPanel.button = button

        local prevHandler = gameRootPanel.onMouseRelease
        multiPanel.prevMouseReleaseHandler = prevHandler
        gameRootPanel.onMouseRelease = function(self, mousePos, mouseButton)
            if mouseButton == MouseRightButton then
                if prevHandler then
                    return prevHandler(self, mousePos, mouseButton)
                end
                return false
            end
            if multiPanel and not multiPanel:isDestroyed() and not multiPanel:containsPoint(mousePos) then
                closeCurrentMultiActionPanel()
            end
            if prevHandler then
                return prevHandler(self, mousePos, mouseButton)
            end
            return false
        end

        button.onGeometryChange = function()
            if not multiPanel or multiPanel:isDestroyed() then
                button.onGeometryChange = nil
                button.onVisibilityChange = nil
                return
            end
            multiPanel:setPosition(getMultiActionPosition(button))
        end
        button.onVisibilityChange = function()
            if not multiPanel or multiPanel:isDestroyed() then
                button.onVisibilityChange = nil
                return
            end
            if not button:isVisible() then
                closeCurrentMultiActionPanel()
            end
        end
        multiPanel:setPosition(getMultiActionPosition(button))
    end

    button.cache = getButtonCache(button)
    if not button.cache.multiActions or table.empty(button.cache.multiActions) then
        button.cache.multiActions = {{}, {}, {}}
    end

    local barID, buttonID = splitButtonId(button)

    if not skipPrefill then
        local multiActionsEmpty = true
        for i = 1, 3 do
            if not table.empty(button.cache.multiActions[i] or {}) then
                multiActionsEmpty = false
                break
            end
        end

        if multiActionsEmpty then
            local prefilled = false
            if button.cache.param and button.cache.param ~= "" then
                local param = button.cache.param
                local sendAutomatic = button.cache.sendAutomatic
                button.cache.multiActions[1] = {
                    chatText = param,
                    sendAutomatically = sendAutomatic
                }
                ApiJson.createOrUpdateMultiText(tonumber(barID), tonumber(buttonID), 1, param, sendAutomatic)
                clearSingleActionCache(button, barID, buttonID)
                prefilled = true
            elseif button.cache.itemId and button.cache.itemId > 100 then
                local itemId = button.cache.itemId
                local useType = getActionName(button.cache.actionType) or "Use"
                local upgradeTier = button.cache.upgradeTier or 0
                local smartMode = button.cache.smartMode or false
                button.cache.multiActions[1] = {
                    useObject = itemId,
                    useType = useType,
                    upgradeTier = upgradeTier,
                    useEquipSmartMode = smartMode
                }
                ApiJson.createOrUpdateMultiAction(tonumber(barID), tonumber(buttonID), 1, useType, itemId, upgradeTier,
                    smartMode)
                clearSingleActionCache(button, barID, buttonID)
                prefilled = true
            end
            if prefilled then
                cacheMultiActionButtons[button] = true
            end
        end
    end

    for k = 1, 3 do
        local actionButton = multiPanel:recursiveGetChildById("actionButton" .. k)
        if actionButton then
            local data = button.cache.multiActions[k] or {}

            actionButton.onMouseRelease = function(self, mousePos, mouseBtn)
                local current = button.cache.multiActions[k]
                onMultiActionButtonMouseRelease(self, mousePos, mouseBtn, button, current)
            end

            resetMultiPanelButton(actionButton)
            actionButton.cache = getButtonCache(actionButton)
            actionButton.multiButtonIndex = k
            actionButton.parentButton = button
            if actionButton.item then
                actionButton.item:setDraggable(true)
                actionButton.item.onDragEnter = function(self, mousePos)
                    local current = button.cache.multiActions[k]
                    if not current or table.empty(current) then
                        return false
                    end
                    if actionButton.cooldown then
                        actionButton.cooldown:setBorderWidth(1)
                    end
                    actionButton.cache.isDragging = true
                    dragButton = actionButton
                    dragItem = self
                    return true
                end
                actionButton.item.onDragMove = function(self, mousePos)
                    self:setPhantom(true)
                    self:setParent(gameRootPanel)
                    self:setX(mousePos.x)
                    self:setY(mousePos.y)
                    self:setBorderColor('white')
                    if lastHighlightWidget then
                        lastHighlightWidget:setBorderWidth(0)
                        lastHighlightWidget:setBorderColor('alpha')
                    end
                    local clickedWidget = gameRootPanel:recursiveGetChildByPos(mousePos, false)
                    if not clickedWidget then
                        return true
                    end
                    lastHighlightWidget = clickedWidget
                    lastHighlightWidget:setBorderWidth(1)
                    lastHighlightWidget:setBorderColor('white')
                end
                actionButton.item.onDragLeave = function(self, widget, mousePos)
                    if not actionButton.cache or not actionButton.cache.isDragging then
                        return false
                    end
                    actionButton.cache.isDragging = false
                    onDragMultiActionItemLeave(self, mousePos, actionButton)
                    dragButton = nil
                    dragItem = nil
                end
            end

            if not table.empty(data) then
                renderSlotOnWidget(actionButton, data, false)
            end
        end
    end
end

-- /*=============================================
-- =         Drag/drop from multi slots          =
-- =============================================*/
local function cancelMultiDrag(self, actionButton)
    if self and not self:isDestroyed() then
        self:setPhantom(false)
        self:setParent(actionButton)
        self:fill('parent')
    end
    if actionButton and actionButton.cooldown then
        actionButton.cooldown:setBorderWidth(0)
    end
end

function onDragMultiActionItemLeave(self, mousePos, actionButton)
    if lastHighlightWidget then
        lastHighlightWidget:setBorderWidth(0)
        lastHighlightWidget:setBorderColor('alpha')
        lastHighlightWidget = nil
    end

    local parentButton = actionButton.parentButton
    local sourceIndex = actionButton.multiButtonIndex
    if not parentButton or not sourceIndex then
        cancelMultiDrag(self, actionButton)
        return
    end

    local clickedWidget = gameRootPanel:recursiveGetChildByPos(mousePos, false)

    if clickedWidget and clickedWidget:getParent() then
        local parentId = clickedWidget:getParent():getId() or ""
        local targetIndex = tonumber(string.match(parentId, "actionButton(%d)"))
        if targetIndex and targetIndex >= 1 and targetIndex <= 3 then
            local panel = clickedWidget:getParent():getParent()
            if panel and panel.button then
                local targetParent = panel.button
                local barID, buttonID = splitButtonId(targetParent)
                if targetParent == parentButton then
                    local sourceData = parentButton.cache.multiActions[sourceIndex]
                    local targetData = parentButton.cache.multiActions[targetIndex]

                    cancelMultiDrag(self, actionButton)

                    parentButton.cache.multiActions[sourceIndex] = targetData or {}
                    parentButton.cache.multiActions[targetIndex] = sourceData or {}

                    persistMultiSlot(tonumber(barID), tonumber(buttonID), sourceIndex, targetData)
                    persistMultiSlot(tonumber(barID), tonumber(buttonID), targetIndex, sourceData)

                    updateMultiButtonState(parentButton)
                    assignMultiAction(parentButton, true)
                    return true
                end

                local targetButton = panel.button
                local tBarID, tButtonID = splitButtonId(targetButton)
                local sBarID, sButtonID = splitButtonId(parentButton)
                local sourceData = parentButton.cache.multiActions[sourceIndex]

                persistMultiSlot(tonumber(tBarID), tonumber(tButtonID), targetIndex, sourceData)

                ApiJson.removeMultiAction(tonumber(sBarID), tonumber(sButtonID), sourceIndex)
                parentButton.cache.multiActions[sourceIndex] = {}

                cancelMultiDrag(self, actionButton)
                updateMultiButtonState(targetButton)
                updateMultiButtonState(parentButton)
                assignMultiAction(targetButton, true)
                assignMultiAction(parentButton, true)
                return true
            end
        end
    end

    if not clickedWidget or not clickedWidget:backwardsGetWidgetById("tabBar") then
        cancelMultiDrag(self, actionButton)
        return
    end

    local destButton = nil
    for _, actionbar in pairs(actionBars) do
        for _, btn in pairs(actionbar.tabBar:getChildren()) do
            if btn:getId() == clickedWidget:getParent():getId() then
                destButton = btn
                break
            end
        end
        if destButton then
            break
        end
    end

    if not destButton then
        cancelMultiDrag(self, actionButton)
        return
    end

    if destButton.cache and hasMultiActions(destButton.cache.multiActions) then
        cancelMultiDrag(self, actionButton)
        return
    end

    local barID, buttonID = splitButtonId(parentButton)
    local dBarID, dButtonID = splitButtonId(destButton)
    local sourceData = parentButton.cache.multiActions[sourceIndex]

    if actionButton.cooldown then
        actionButton.cooldown:setBorderWidth(0)
        actionButton.cooldown:setBorderColor('alpha')
    end

    local destHasAction = false
    if destButton.cache.itemId and destButton.cache.itemId > 100 then
        destHasAction = true
        local actionTypeName = localGetActionName(destButton.cache.actionType)
        if actionTypeName then
            ApiJson.createOrUpdateMultiAction(tonumber(barID), tonumber(buttonID), sourceIndex, actionTypeName,
                destButton.cache.itemId, destButton.cache.upgradeTier or 0, destButton.cache.smartMode or false)
            parentButton.cache.multiActions[sourceIndex] = {
                useObject = destButton.cache.itemId,
                useType = actionTypeName,
                upgradeTier = destButton.cache.upgradeTier or 0,
                useEquipSmartMode = destButton.cache.smartMode or false
            }
        end
    elseif destButton.cache.param and destButton.cache.param ~= "" then
        destHasAction = true
        ApiJson.createOrUpdateMultiText(tonumber(barID), tonumber(buttonID), sourceIndex, destButton.cache.param,
            destButton.cache.sendAutomatic)
        parentButton.cache.multiActions[sourceIndex] = {
            chatText = destButton.cache.param,
            sendAutomatically = destButton.cache.sendAutomatic
        }
    end

    if sourceData and not table.empty(sourceData) then
        if sourceData["chatText"] then
            ApiJson.createOrUpdateText(tonumber(dBarID), tonumber(dButtonID), sourceData["chatText"],
                sourceData["sendAutomatically"])
        elseif sourceData["useObject"] then
            local actionTypeName = localGetActionName(sourceData["useType"]) or "Use"
            ApiJson.createOrUpdateAction(tonumber(dBarID), tonumber(dButtonID), actionTypeName, sourceData["useObject"],
                sourceData["upgradeTier"] or 0)
        end
    end

    if not destHasAction then
        ApiJson.removeMultiAction(tonumber(barID), tonumber(buttonID), sourceIndex)
        parentButton.cache.multiActions[sourceIndex] = {}

        if not hasMultiActions(parentButton.cache.multiActions) then
            parentButton.cache.itemId = 0
            parentButton.cache.param = ""
            parentButton.cache.actionType = 0
            if parentButton.multiIcon then
                parentButton.multiIcon:setVisible(false)
            end
            cacheMultiActionButtons[parentButton] = nil
            clearMultiActionCooldownEvents(parentButton:getId())
        end
    end

    cancelMultiDrag(self, actionButton)
    updateButton(destButton)
    updateMultiButtonState(parentButton)
    assignMultiAction(parentButton, true)
end
