HOTKEY_MANAGER_USE = nil
HOTKEY_MANAGER_USEONSELF = 1
HOTKEY_MANAGER_USEONTARGET = 2
HOTKEY_MANAGER_USEWITH = 3
HOTKEY_MANAGER_USEATCURSOR = 4

HOTKEY_ACTION_TOGGLE_WASD = 1
HOTKEY_ACTION_ATTACK_NEXT = 2
HOTKEY_ACTION_ATTACK_PREV = 3
HOTKEY_ACTION_TOGGLE_CHASE = 4

HotkeyActions = {{
    id = HOTKEY_ACTION_TOGGLE_WASD,
    text = tr('Toggle WASD chat mode')
}, {
    id = HOTKEY_ACTION_ATTACK_NEXT,
    text = tr('Attack next creature in battle list')
}, {
    id = HOTKEY_ACTION_ATTACK_PREV,
    text = tr('Attack previous creature in battle list')
}, {
    id = HOTKEY_ACTION_TOGGLE_CHASE,
    text = tr('Toggle chase mode')
}}

HotkeyColors = {
    text = '#888888',
    textAutoSend = '#FFFFFF',
    itemUse = '#8888FF',
    itemUseSelf = '#00FF00',
    itemUseTarget = '#FF0000',
    itemUseWith = '#F5B325',
    action = '#F97ACD'
}

hotkeysManagerLoaded = false
hotkeysWindow = nil

currentHotkeyLabel = nil
currentItemPreview = nil
itemWidget = nil
addHotkeyButton = nil
removeHotkeyButton = nil
hotkeyActionCombo = nil
hotkeyText = nil
hotKeyTextLabel = nil
sendAutomatically = nil
selectObjectButton = nil
clearObjectButton = nil
useOnSelf = nil
useOnTarget = nil
useWith = nil
useAtCursor = nil
defaultComboKeys = nil
perServer = true
perCharacter = true
mouseGrabberWidget = nil
useRadioGroup = nil
currentHotkeys = nil
boundCombosCallback = {}
hotkeysList = {}
local hotkeyBlockingSources = {}
local nextSourceId = 1
lastHotkeyTime = g_clock.millis()
local hotkeysWindowButton = nil

-- public functions
function init()

    Keybind.new("Windows", "Show/hide Hotkeys", "Ctrl+K", "")
    Keybind.bind("Windows", "Show/hide Hotkeys", {
      {
        type = KEY_DOWN,
        callback = toggle,
      }
    })
    hotkeysWindow = g_ui.displayUI('/game_hotkeys/hotkeys_manager')
    hotkeysWindow:setVisible(false)
    hotkeysWindowButton = modules.client_topmenu.addRightGameToggleButton('hotkeysWindowButton', tr('Hotkeys'), '/images/options/hotkeys', toggle)

    currentHotkeys = hotkeysWindow:getChildById('currentHotkeys')
    currentItemPreview = hotkeysWindow:getChildById('itemPreview')
    addHotkeyButton = hotkeysWindow:getChildById('addHotkeyButton')
    removeHotkeyButton = hotkeysWindow:getChildById('removeHotkeyButton')
    hotkeyText = hotkeysWindow:getChildById('hotkeyText')
    hotKeyTextLabel = hotkeysWindow:getChildById('hotKeyTextLabel')
    sendAutomatically = hotkeysWindow:getChildById('sendAutomatically')
    selectObjectButton = hotkeysWindow:getChildById('selectObjectButton')
    clearObjectButton = hotkeysWindow:getChildById('clearObjectButton')
    useOnSelf = hotkeysWindow:getChildById('useOnSelf')
    useOnTarget = hotkeysWindow:getChildById('useOnTarget')
    useWith = hotkeysWindow:getChildById('useWith')
    useAtCursor = hotkeysWindow:getChildById('useAtCursor')

    useRadioGroup = UIRadioGroup.create()
    useRadioGroup:addWidget(useOnSelf)
    useRadioGroup:addWidget(useOnTarget)
    useRadioGroup:addWidget(useWith)
    useRadioGroup:addWidget(useAtCursor)
    useRadioGroup.onSelectionChange = function(self, selected)
        onChangeUseType(selected)
    end

    hotkeyActionCombo = hotkeysWindow:getChildById('hotkeyActionCombo')

    hotkeyActionCombo:addOption('None', 0)
    for _, action in pairs(HotkeyActions) do
        hotkeyActionCombo:addOption(action.text, action.id)
    end

    hotkeyActionCombo.onOptionChange = onActionChange

    mouseGrabberWidget = g_ui.createWidget('UIWidget')
    mouseGrabberWidget:setVisible(false)
    mouseGrabberWidget:setFocusable(false)
    mouseGrabberWidget.onMouseRelease = onChooseItemMouseRelease

    currentHotkeys.onChildFocusChange = function(self, hotkeyLabel)
        onSelectHotkeyLabel(hotkeyLabel)
    end
    g_keyboard.bindKeyPress('Down', function()
        currentHotkeys:focusNextChild(KeyboardFocusReason)
    end, hotkeysWindow)
    g_keyboard.bindKeyPress('Up', function()
        currentHotkeys:focusPreviousChild(KeyboardFocusReason)
    end, hotkeysWindow)

    connect(g_game, {
        onGameStart = online,
        onGameEnd = offline
    })

    load()
end

function terminate()
    disconnect(g_game, {
        onGameStart = online,
        onGameEnd = offline
    })

    Keybind.delete("Windows", "Show/hide Hotkeys")

    unload()

    hotkeysWindow:destroy()

    mouseGrabberWidget:destroy()
    hotkeysWindow = nil

    hotkeyActionCombo = nil
    hotKeyTextLabel = nil
    hotkeyText = nil
    sendAutomatically = nil
    selectObjectButton = nil
    clearObjectButton = nil
    mouseGrabberWidget = nil
    addHotkeyButton = nil
    removeHotkeyButton = nil
    itemPreview = nil
    useOnSelf = nil
    useOnTarget = nil
    useWith = nil
    useAtCursor = nil
    currentHotkeys = nil
    hotkeysWindowButton = nil
end

function configure(savePerServer, savePerCharacter)
    perServer = savePerServer
    perCharacter = savePerCharacter
    reload()
end

function online()
    reload()
    hide()
end

function offline()
    unload()
    hide()
end

function show()
    if not g_game.isOnline() then
        return
    end
    hotkeysWindow:show()
    hotkeysWindow:raise()
    hotkeysWindow:focus()
    if hotkeysWindowButton then
        hotkeysWindowButton:setOn(true)
    end
end

function hide()
    hotkeysWindow:hide()
    if hotkeysWindowButton then
        hotkeysWindowButton:setOn(false)
    end
end

function toggle()
    if not hotkeysWindow:isVisible() then
        show()
    else
        hide()
    end
end

function ok()
    save()
    hide()
end

function cancel()
    -- Pythera: closing the window used to reload from disk, silently throwing
    -- away everything configured in this session - the usual way people lost
    -- their hotkeys. Closing now persists, same as pressing Ok.
    save()
    hide()
end

function load(forceDefaults)
    local serverHost = nil
    hotkeysManagerLoaded = false

    local hotkeySettings = g_settings.getNode('game_hotkeys')
    local hotkeys = {}

    if not table.empty(hotkeySettings) then
        hotkeys = hotkeySettings
    end
    if perServer and not table.empty(hotkeys) then
        if G.host ~= nil then
            serverHost = string.gsub(G.host, "^https?://", "")
            hotkeys = hotkeys[serverHost]
        end
    end
    if perCharacter and not table.empty(hotkeys) then
        hotkeys = hotkeys[g_game.getCharacterName()]
    end

    hotkeyList = {}
    if not forceDefaults then
        if not table.empty(hotkeys) then
            for keyCombo, setting in pairs(hotkeys) do
                keyCombo = tostring(keyCombo)
                addKeyCombo(keyCombo, setting)
                hotkeyList[keyCombo] = setting
            end
        end
    end

    if currentHotkeys:getChildCount() == 0 then
        loadDefautComboKeys()
    end

    hotkeysManagerLoaded = true
end

function unload()
    for keyCombo, callback in pairs(boundCombosCallback) do
        g_keyboard.unbindKeyPress(keyCombo, callback)
    end
    boundCombosCallback = {}
    currentHotkeys:destroyChildren()
    currentHotkeyLabel = nil
    updateHotkeyForm(true)
    hotkeyList = {}
end

function reset()
    unload()
    load(true)
end

function reload()
    unload()
    load()
end

function save()
    local serverHost = string.gsub(G.host, "^https?://", "")
    local hotkeySettings = g_settings.getNode('game_hotkeys') or {}
    local hotkeys = hotkeySettings

    if perServer then
        if not hotkeys[serverHost] then
            hotkeys[serverHost] = {}
        end
        hotkeys = hotkeys[serverHost]
    end

    if perCharacter then
        local char = g_game.getCharacterName()
        if not hotkeys[char] then
            hotkeys[char] = {}
        end
        hotkeys = hotkeys[char]
    end

    table.clear(hotkeys)

    for _, child in pairs(currentHotkeys:getChildren()) do
        hotkeys[child.keyCombo] = {
            autoSend = child.autoSend,
            itemId = child.itemId,
            subType = child.subType,
            useType = child.useType,
            value = child.value,
            action = child.action
        }
    end

    hotkeyList = hotkeys
    g_settings.setNode('game_hotkeys', hotkeySettings)
    g_settings.save()
end

function loadDefautComboKeys()
    if not defaultComboKeys then
        for i = 1, 12 do
            addKeyCombo('F' .. i)
        end
        for i = 1, 4 do
            addKeyCombo('Shift+F' .. i)
        end
    else
        for keyCombo, keySettings in pairs(defaultComboKeys) do
            addKeyCombo(keyCombo, keySettings)
        end
    end
end

function setDefaultComboKeys(combo)
    defaultComboKeys = combo
end

function onActionChange(comboBox, option)
    local action = comboBox:getCurrentOption().data
    if currentHotkeyLabel then
        if action > 0 then
            currentHotkeyLabel.action = action
            currentHotkeyLabel.itemId = nil
            currentHotkeyLabel.value = nil
            currentHotkeyLabel.autoSend = nil
        else
            currentHotkeyLabel.action = nil
        end
        updateHotkeyLabel(currentHotkeyLabel)
        updateHotkeyForm(true, true)
    end
end

function onChooseItemMouseRelease(self, mousePosition, mouseButton)
    local item = nil
    if mouseButton == MouseLeftButton then
        local clickedWidget = modules.game_interface.getRootPanel():recursiveGetChildByPos(mousePosition, false)
        if clickedWidget then
            if clickedWidget:getClassName() == 'UIGameMap' then
                local tile = clickedWidget:getTile(mousePosition)
                if tile then
                    local thing = tile:getTopMoveThing()
                    if thing and thing:isItem() then
                        item = thing
                    end
                end
            elseif clickedWidget:getClassName() == 'UIItem' and not clickedWidget:isVirtual() then
                item = clickedWidget:getItem()
            end
        end
    end

    if item and currentHotkeyLabel then
        currentHotkeyLabel.itemId = item:getId()
        if item:isFluidContainer() then
            currentHotkeyLabel.subType = item:getSubType()
        end
        if item:isMultiUse() then
            currentHotkeyLabel.useType = HOTKEY_MANAGER_USEWITH
        else
            currentHotkeyLabel.useType = HOTKEY_MANAGER_USE
        end
        currentHotkeyLabel.value = nil
        currentHotkeyLabel.autoSend = false
        updateHotkeyLabel(currentHotkeyLabel)
        updateHotkeyForm(true)
    end

    show()

    -- Restore cursor
    if modules.client_options and modules.client_options.getOption('nativeCursor') then
        g_window.restoreMouseCursor()
    else
        g_mouse.popCursor('target')
    end
    self:ungrabMouse()
    return true
end

function startChooseItem()
    if g_ui.isMouseGrabbed() then
        return
    end
    mouseGrabberWidget:grabMouse()
    -- Use native cursor when enabled, otherwise use custom cursor
    if modules.client_options and modules.client_options.getOption('nativeCursor') then
        g_window.setSystemCursor('cross')
    else
        g_mouse.pushCursor('target')
    end
    hide()
end

function clearObject()
    currentHotkeyLabel.itemId = nil
    currentHotkeyLabel.subType = nil
    currentHotkeyLabel.useType = nil
    currentHotkeyLabel.autoSend = nil
    currentHotkeyLabel.value = nil
    currentHotkeyLabel.action = nil
    updateHotkeyLabel(currentHotkeyLabel)
    updateHotkeyForm(true)
end

function addHotkey()
    local assignWindow = g_ui.createWidget('HotkeyAssignWindow', rootWidget)
    assignWindow:grabKeyboard()

    local comboLabel = assignWindow:getChildById('comboPreview')
    comboLabel.keyCombo = ''
    assignWindow.onKeyDown = hotkeyCapture
end

function addKeyCombo(keyCombo, keySettings, focus)
    if keyCombo == nil or #keyCombo == 0 then
        return
    end
    if not keyCombo then
        return
    end
    if modules.game_actionbar and modules.game_actionbar.removeHotkeyFromActionBar then
        modules.game_actionbar.removeHotkeyFromActionBar(keyCombo)
    end
    local hotkeyLabel = currentHotkeys:getChildById(keyCombo)
    if not hotkeyLabel then
        hotkeyLabel = g_ui.createWidget('HotkeyListLabel')
        hotkeyLabel:setId(keyCombo)

        local children = currentHotkeys:getChildren()
        children[#children + 1] = hotkeyLabel
        table.sort(children, function(a, b)
            if a:getId():len() < b:getId():len() then
                return true
            elseif a:getId():len() == b:getId():len() then
                return a:getId() < b:getId()
            else
                return false
            end
        end)
        for i = 1, #children do
            if children[i] == hotkeyLabel then
                currentHotkeys:insertChild(i, hotkeyLabel)
                break
            end
        end

        if keySettings then
            currentHotkeyLabel = hotkeyLabel
            hotkeyLabel.keyCombo = keyCombo
            hotkeyLabel.autoSend = toboolean(keySettings.autoSend)
            hotkeyLabel.itemId = tonumber(keySettings.itemId)
            hotkeyLabel.subType = tonumber(keySettings.subType)
            hotkeyLabel.useType = tonumber(keySettings.useType)
            hotkeyLabel.action = tonumber(keySettings.action)
            if keySettings.value then
                hotkeyLabel.value = tostring(keySettings.value)
            end
        else
            hotkeyLabel.keyCombo = keyCombo
            hotkeyLabel.autoSend = false
            hotkeyLabel.itemId = nil
            hotkeyLabel.subType = nil
            hotkeyLabel.useType = nil
            hotkeyLabel.action = nil
            hotkeyLabel.value = ''
        end

        updateHotkeyLabel(hotkeyLabel)

        boundCombosCallback[keyCombo] = function()
            doKeyCombo(keyCombo)
        end
        g_keyboard.bindKeyPress(keyCombo, boundCombosCallback[keyCombo])
    end

    if focus then
        currentHotkeys:focusChild(hotkeyLabel)
        currentHotkeys:ensureChildVisible(hotkeyLabel)
        updateHotkeyForm(true)
    end
end

function doKeyCombo(keyCombo)
    if not g_game.isOnline() then
        return
    end
    if not canPerformKeyCombo(keyCombo) then
        return
    end
    local hotKey = hotkeyList[keyCombo]
    if not hotKey then
        return
    end

    if g_clock.millis() - lastHotkeyTime < modules.client_options.getOption('hotkeyDelay') then
        return
    end
    lastHotkeyTime = g_clock.millis()

    if hotKey.action then
        if hotKey.action == HOTKEY_ACTION_TOGGLE_WASD then
            modules.game_console.toggleChat()
        elseif hotKey.action == HOTKEY_ACTION_ATTACK_NEXT then
            modules.game_battle.attackNext()
        elseif hotKey.action == HOTKEY_ACTION_ATTACK_PREV then
            modules.game_battle.attackNext(true)
        elseif hotKey.action == HOTKEY_ACTION_TOGGLE_CHASE then
            toggleChaseMode()
        end

    elseif hotKey.itemId == nil then
        if not hotKey.value or #hotKey.value == 0 then
            return
        end
        if hotKey.autoSend then
            modules.game_console.sendMessage(hotKey.value)
        else
            scheduleEvent(function()
                if not modules.game_console.isChatEnabled() then
                    modules.game_console.switchChatOnCall()
                end
                modules.game_console.setTextEditText(hotKey.value)
            end, 1)
        end
    else
        executeHotkeyItem(hotKey.useType, hotKey.itemId, hotKey.subType)
    end
end

function toggleChaseMode()
    local currentMode = g_game.getChaseMode()
    local nextMode = currentMode == ChaseOpponent and DontChase or ChaseOpponent
    g_game.setChaseMode(nextMode)
end

function executeHotkeyItem(action, itemId, subType)
    local function get_use_thing_under_cursor()
        local mapPanel = modules.game_interface and modules.game_interface.getMapPanel and modules.game_interface.getMapPanel()
        if not mapPanel then
            return nil
        end

        local mousePosition = g_window.getMousePosition()
        if not mapPanel:containsPoint(mousePosition) then
            return nil
        end

        local mapPosition = mapPanel:getPosition(mousePosition)
        if not mapPosition then
            return nil
        end

        local localPlayer = g_game.getLocalPlayer()
        if localPlayer and mapPosition.z ~= localPlayer:getPosition().z then
            local dz = mapPosition.z - localPlayer:getPosition().z
            mapPosition.x = mapPosition.x + dz
            mapPosition.y = mapPosition.y + dz
            mapPosition.z = localPlayer:getPosition().z
        end

        local tile = g_map.getTile(mapPosition)
        if not tile then
            return nil
        end

        local virtualItem = Item.create(itemId)
        if virtualItem:isFluidContainer() or virtualItem:isMultiUse() then
            return tile:getTopMultiUseThing()
        end
        return tile:getTopUseThing()
    end

    if action == HOTKEY_MANAGER_USE then
        if g_game.getClientVersion() < 780 or subType then
            local item = g_game.findPlayerItem(itemId, subType or -1)
            if item then
                g_game.use(item)
            end
        else
            g_game.useInventoryItem(itemId)
        end
    elseif action == HOTKEY_MANAGER_USEONSELF then
        if g_game.getClientVersion() < 780 or subType then
            local item = g_game.findPlayerItem(itemId, subType or -1)
            if item then
                g_game.useWith(item, g_game.getLocalPlayer())
            end
        else
            g_game.useInventoryItemWith(itemId, g_game.getLocalPlayer())
        end
    elseif action == HOTKEY_MANAGER_USEONTARGET then
        local attackingCreature = g_game.getAttackingCreature()
        if not attackingCreature then
            local item = Item.create(itemId)
            if g_game.getClientVersion() < 780 or subType then
                local tmpItem = g_game.findPlayerItem(itemId, subType or -1)
                if not tmpItem then
                    return
                end
                item = tmpItem
            end

            modules.game_interface.startUseWith(item)
            return
        end

        if not attackingCreature:getTile() then
            return
        end
        if g_game.getClientVersion() < 780 or subType then
            local item = g_game.findPlayerItem(itemId, subType or -1)
            if item then
                g_game.useWith(item, attackingCreature)
            end
        else
            g_game.useInventoryItemWith(itemId, attackingCreature)
        end
    elseif action == HOTKEY_MANAGER_USEWITH then
        local item = Item.create(itemId)
        if g_game.getClientVersion() < 780 or subType then
            local tmpItem = g_game.findPlayerItem(itemId, subType or -1)
            if not tmpItem then
                return true
            end
            item = tmpItem
        end
        modules.game_interface.startUseWith(item)
    elseif action == HOTKEY_MANAGER_USEATCURSOR then
        local useThing = get_use_thing_under_cursor()
        if not useThing then
            local item = Item.create(itemId)
            if g_game.getClientVersion() < 780 or subType then
                local tmpItem = g_game.findPlayerItem(itemId, subType or -1)
                if not tmpItem then
                    return
                end
                item = tmpItem
            end
            modules.game_interface.startUseWith(item)
            return
        end

        if g_game.getClientVersion() < 780 or subType then
            local item = g_game.findPlayerItem(itemId, subType or -1)
            if item then
                g_game.useWith(item, useThing)
            end
        else
            g_game.useInventoryItemWith(itemId, useThing)
        end
    end
end

function updateHotkeyLabel(hotkeyLabel)
    if not hotkeyLabel then
        return
    end
    if hotkeyLabel.useType == HOTKEY_MANAGER_USEONSELF then
        hotkeyLabel:setText(tr('%s: (use object on yourself)', hotkeyLabel.keyCombo))
        hotkeyLabel:setColor(HotkeyColors.itemUseSelf)
    elseif hotkeyLabel.useType == HOTKEY_MANAGER_USEONTARGET then
        hotkeyLabel:setText(tr('%s: (use object on target)', hotkeyLabel.keyCombo))
        hotkeyLabel:setColor(HotkeyColors.itemUseTarget)
    elseif hotkeyLabel.useType == HOTKEY_MANAGER_USEWITH then
        hotkeyLabel:setText(tr('%s: (use object with crosshair)', hotkeyLabel.keyCombo))
        hotkeyLabel:setColor(HotkeyColors.itemUseWith)
    elseif hotkeyLabel.useType == HOTKEY_MANAGER_USEATCURSOR then
        hotkeyLabel:setText(tr('%s: (use object at cursor position)', hotkeyLabel.keyCombo))
        hotkeyLabel:setColor(HotkeyColors.itemUseWith)
    elseif hotkeyLabel.itemId ~= nil then
        hotkeyLabel:setText(tr('%s: (use object)', hotkeyLabel.keyCombo))
        hotkeyLabel:setColor(HotkeyColors.itemUse)
    elseif hotkeyLabel.action then
        for _, action in pairs(HotkeyActions) do
            if action.id == hotkeyLabel.action then
                hotkeyLabel:setText(tr('%s: ' .. action.text, hotkeyLabel.keyCombo))
                break
            end
        end
        hotkeyLabel:setColor(HotkeyColors.action)
    else
        local text = hotkeyLabel.keyCombo .. ': '
        if hotkeyLabel.value then
            text = text .. hotkeyLabel.value
        end
        hotkeyLabel:setText(text)
        if hotkeyLabel.autoSend then
            hotkeyLabel:setColor(HotkeyColors.autoSend)
        else
            hotkeyLabel:setColor(HotkeyColors.text)
        end
    end
end

function updateHotkeyForm(reset, dontUpdateCombo)
    if currentHotkeyLabel then
        removeHotkeyButton:enable()
        if currentHotkeyLabel.itemId ~= nil then
            if not dontUpdateCombo then
                hotkeyActionCombo:setCurrentIndex(1)
            end
            hotkeyActionCombo:disable()
            hotkeyText:clearText()
            hotkeyText:disable()
            hotKeyTextLabel:disable()
            sendAutomatically:setChecked(false)
            sendAutomatically:disable()
            selectObjectButton:disable()
            clearObjectButton:enable()
            currentItemPreview:setItemId(currentHotkeyLabel.itemId)
            if currentHotkeyLabel.subType then
                currentItemPreview:setItemSubType(currentHotkeyLabel.subType)
            end
            if currentItemPreview:getItem():isMultiUse() then
                useOnSelf:enable()
                useOnTarget:enable()
                useWith:enable()
                useAtCursor:enable()
                if currentHotkeyLabel.useType == HOTKEY_MANAGER_USEONSELF then
                    useRadioGroup:selectWidget(useOnSelf)
                elseif currentHotkeyLabel.useType == HOTKEY_MANAGER_USEONTARGET then
                    useRadioGroup:selectWidget(useOnTarget)
                elseif currentHotkeyLabel.useType == HOTKEY_MANAGER_USEWITH then
                    useRadioGroup:selectWidget(useWith)
                elseif currentHotkeyLabel.useType == HOTKEY_MANAGER_USEATCURSOR then
                    useRadioGroup:selectWidget(useAtCursor)
                end
            else
                useOnSelf:disable()
                useOnTarget:disable()
                useWith:disable()
                useAtCursor:disable()
                useRadioGroup:clearSelected()
            end
        elseif currentHotkeyLabel.action then
            if not dontUpdateCombo then
                hotkeyActionCombo:setCurrentOptionByData(currentHotkeyLabel.action)
            end
            hotkeyActionCombo:enable()
            hotkeyText:clearText()
            hotkeyText:disable()
            hotKeyTextLabel:disable()
            sendAutomatically:setChecked(false)
            sendAutomatically:disable()
            selectObjectButton:disable()
            useOnSelf:disable()
            useOnTarget:disable()
            useWith:disable()
            useAtCursor:disable()
            useRadioGroup:clearSelected()
            selectObjectButton:disable()
            clearObjectButton:disable()
        else
            if not dontUpdateCombo then
                hotkeyActionCombo:setCurrentIndex(1)
            end
            hotkeyActionCombo:enable()
            useOnSelf:disable()
            useOnTarget:disable()
            useWith:disable()
            useAtCursor:disable()
            useRadioGroup:clearSelected()
            hotkeyText:enable()
            hotkeyText:focus()
            hotKeyTextLabel:enable()
            hotkeyText:setText(currentHotkeyLabel.value)
            if reset then
                hotkeyText:setCursorPos(-1)
            end
            sendAutomatically:setChecked(currentHotkeyLabel.autoSend)
            sendAutomatically:setEnabled(currentHotkeyLabel.value and #currentHotkeyLabel.value > 0)
            selectObjectButton:enable()
            clearObjectButton:disable()
            currentItemPreview:clearItem()
        end
    else
        if not dontUpdateCombo then
            hotkeyActionCombo:setCurrentIndex(1)
        end
        hotkeyActionCombo:disable()
        removeHotkeyButton:disable()
        hotkeyText:disable()
        sendAutomatically:disable()
        selectObjectButton:disable()
        clearObjectButton:disable()
        useOnSelf:disable()
        useOnTarget:disable()
        useWith:disable()
        useAtCursor:disable()
        hotkeyText:clearText()
        useRadioGroup:clearSelected()
        sendAutomatically:setChecked(false)
        currentItemPreview:clearItem()
    end
end

function removeHotkey()
    if currentHotkeyLabel == nil then
        return
    end
    g_keyboard.unbindKeyPress(currentHotkeyLabel.keyCombo, boundCombosCallback[currentHotkeyLabel.keyCombo])
    boundCombosCallback[currentHotkeyLabel.keyCombo] = nil
    currentHotkeyLabel:destroy()
    currentHotkeyLabel = nil
    -- Pythera: persist right away so a crash or an unexpected exit can't
    -- roll the change back
    save()
end

function onHotkeyTextChange(value)
    if not hotkeysManagerLoaded then
        return
    end
    if currentHotkeyLabel == nil then
        return
    end
    currentHotkeyLabel.value = value
    if value == '' then
        currentHotkeyLabel.autoSend = false
    end
    updateHotkeyLabel(currentHotkeyLabel)
    updateHotkeyForm(false, true)
end

function onSendAutomaticallyChange(autoSend)
    if not hotkeysManagerLoaded then
        return
    end
    if currentHotkeyLabel == nil then
        return
    end
    if not currentHotkeyLabel.value or #currentHotkeyLabel.value == 0 then
        return
    end
    currentHotkeyLabel.autoSend = autoSend
    updateHotkeyLabel(currentHotkeyLabel)
    updateHotkeyForm(false, true)
end

function onChangeUseType(useTypeWidget)
    if not hotkeysManagerLoaded then
        return
    end
    if currentHotkeyLabel == nil then
        return
    end
    if useTypeWidget == useOnSelf then
        currentHotkeyLabel.useType = HOTKEY_MANAGER_USEONSELF
    elseif useTypeWidget == useOnTarget then
        currentHotkeyLabel.useType = HOTKEY_MANAGER_USEONTARGET
    elseif useTypeWidget == useWith then
        currentHotkeyLabel.useType = HOTKEY_MANAGER_USEWITH
    elseif useTypeWidget == useAtCursor then
        currentHotkeyLabel.useType = HOTKEY_MANAGER_USEATCURSOR
    else
        currentHotkeyLabel.useType = HOTKEY_MANAGER_USE
    end
    updateHotkeyLabel(currentHotkeyLabel)
    updateHotkeyForm()
end

function onSelectHotkeyLabel(hotkeyLabel)
    currentHotkeyLabel = hotkeyLabel
    updateHotkeyForm(true)
end

function hotkeyCapture(assignWindow, keyCode, keyboardModifiers)
    local keyCombo = determineKeyComboDesc(keyCode, keyboardModifiers)
    local comboPreview = assignWindow:getChildById('comboPreview')
    comboPreview:setText(tr('Current hotkey to add: %s', keyCombo))
    comboPreview.keyCombo = keyCombo
    comboPreview:resizeToText()
    assignWindow:getChildById('addButton'):enable()
    return true
end

function hotkeyCaptureOk(assignWindow, keyCombo)
    addKeyCombo(keyCombo, nil, true)
    assignWindow:destroy()
end

function enableHotkeys(sourceId)
    if sourceId then
        hotkeyBlockingSources[sourceId] = nil
    end
end

function disableHotkeys(sourceIdentifier)
    local sourceId = sourceIdentifier or ("auto_" .. nextSourceId)
    nextSourceId = nextSourceId + 1
    hotkeyBlockingSources[sourceId] = true
    return sourceId
end

local function getCallerModule()
    local info = debug.getinfo(3, "S")
    if info and info.source then
        local source = info.source:gsub("@", "")
        local moduleName = source:match("/modules/([^/]+)/") or 
                          source:match("\\modules\\([^\\]+)\\") or
                          source:match("([^/\\]+)%.lua$") or
                          "unknown"
        return moduleName:gsub("_", "")
    end
    return "unknown"
end

function createHotkeyBlock(sourceIdentifier)
    local callerModule = getCallerModule()
    local fullId = sourceIdentifier and 
        (sourceIdentifier .. "_" .. callerModule) or 
        ("auto_" .. callerModule .. "_" .. nextSourceId)
    local blockId = disableHotkeys(fullId)
    return {
        release = function()
            enableHotkeys(blockId)
        end,
        getId = function()
            return fullId
        end
    }
end

function areHotkeysDisabled()
    for _ in pairs(hotkeyBlockingSources) do
        return true
    end
    return false
end

function clearAllHotkeyBlocks()
    hotkeyBlockingSources = {}
end
function getHotkeyBlockingInfo()
    local count = 0
    local sources = {}
    for sourceId in pairs(hotkeyBlockingSources) do
        count = count + 1
        table.insert(sources, sourceId)
    end
    table.sort(sources)
    return count, sources
end

function printHotkeyBlockingInfo()
    local count, sources = getHotkeyBlockingInfo()
    print("=== Hotkey Blocking Info ===")
    print("Total blocks: " .. count)
    if count > 0 then
        print("Active sources:")
        for i, source in ipairs(sources) do
            print("  " .. i .. ". " .. source)
        end
    else
        print("No active blocks")
    end
    print("===========================")
end

-- Even if hotkeys are enabled, only the hotkeys containing Ctrl or Alt or F1-F12 will be enabled when
-- chat is opened (no WASD mode). This is made to prevent executing hotkeys while typing...
function canPerformKeyCombo(keyCombo)
    if areHotkeysDisabled() then
        return false
    end
    if not modules.game_console.isChatEnabled() then
        return true
    end
    local platformType = g_window.getPlatformType() or ""
    local isMacOS = platformType:find("MACOS") ~= nil
    if isMacOS then
        return  string.match(keyCombo, "Cmd%+") or
                string.match(keyCombo, "Ctrl%+") or
                string.match(keyCombo, "Alt%+") or
                string.match(keyCombo, "Option%+") or
                string.match(keyCombo, "F%d+")
    end
    return  string.match(keyCombo, "Ctrl%+") or
            string.match(keyCombo, "Alt%+") or
            string.match(keyCombo, "F%d+")
end

-- Actionbar
function removeHotkeyByCombo(keyCombo)
    if not keyCombo or keyCombo == "" then
        return false
    end
    local hotkeyLabel = currentHotkeys and currentHotkeys:getChildById(keyCombo)
    if hotkeyLabel then
        if boundCombosCallback[keyCombo] then
            g_keyboard.unbindKeyPress(keyCombo, boundCombosCallback[keyCombo])
            boundCombosCallback[keyCombo] = nil
        end
        if currentHotkeyLabel == hotkeyLabel then
            currentHotkeyLabel = nil
        end
        hotkeyLabel:destroy()
        updateHotkeyForm(true)
        -- Pythera: this removal only touched the in-memory widget tree.
        -- Without persisting here, the deletion silently lands on disk
        -- whenever something else calls save() later, looking like a
        -- random hotkey reset.
        save()
        return true
    end
    return false
end

function isHotkeyUsedByManager(keyCombo)
    if not keyCombo or keyCombo == "" then
        return false
    end
    if boundCombosCallback[keyCombo] then
        return true
    end
    if currentHotkeys then
        local hotkeyLabel = currentHotkeys:getChildById(keyCombo)
        if hotkeyLabel then
            return true
        end
    end
    return false
end
