-- Global variables
local binaryTree = {}
local battleButtons = {}
local battleWindow, battleButton, battlePanel, mouseWidget, filterPanel, toggleFilterButton
local lastBattleButtonSwitched, lastCreatureSelected
local hideButtons = {}
local eventOnCheckCreature = nil
local eventsConnected = false

-- Forward declarations
local onBattleButtonHoverChange, onBattleButtonMouseRelease
BattleListInstance = nil
BattleButtonPool = nil

-- Utility functions
function table.copy(t)
    if type(t) ~= "table" then return t end
    local meta = getmetatable(t)
    local target = {}
    for k, v in pairs(t) do
        target[k] = type(v) == "table" and table.copy(v) or v
    end
    setmetatable(target, meta)
    return target
end

function table.size(t)
    if type(t) ~= "table" then return 0 end
    local count = 0
    for _ in pairs(t) do count = count + 1 end
    return count
end

-- Default filter settings
local BATTLE_FILTERS = {
    ["sortAscByDisplayTime"] = true,
    ["sortDescByDisplayTime"] = false,
    ["sortAscByDistance"] = false,
    ["sortDescByDistance"] = false,
    ["sortAscByHitPoints"] = false,
    ["sortDescByHitPoints"] = false,
    ["sortAscByName"] = false,
    ["sortDescByName"] = false
}

-- Battle List Manager
local BattleListManager = {
    instances = {},
    nextId = 1,
    autoSaveEvent = nil,
    isRestoring = false
}

function BattleListManager:saveInstancesState()
    local instancesData = {}
    for id, instance in pairs(self.instances) do
        if id ~= 0 then
            local windowPos = instance.window and instance.window:getPosition()
            local windowSize = instance.window and instance.window:getSize()
            local isLocked = false
            local isMinimized = false
            if instance.window then
                isLocked = instance.window:getSettings('locked') or false
                local lockButton = instance.window:getChildById('lockButton')
                if lockButton then
                    isLocked = lockButton:isOn()
                end
                -- Check if window is minimized (visible but collapsed)
                if instance.window:isVisible() then
                    isMinimized = instance.window:isOn()
                end
            end
            instancesData[id] = {
                id = instance.id,
                name = instance:getName(),
                isOpen = instance.window and instance.window:isVisible(),
                isMinimized = isMinimized,
                windowPos = windowPos,
                windowSize = windowSize,
                isHidingFilters = instance:isHidingFilters(),
                isLocked = isLocked
            }
            instance:saveFilters()
            instance:saveHideButtonStates()
            instance:saveLockState()
        end
    end

    local mainInstance = self.instances[0]
    if mainInstance then
        mainInstance:saveLockState()
        mainInstance:saveFilters()
        mainInstance:saveHideButtonStates()
    end

    g_settings.mergeNode('BattleListInstances', instancesData)
end

function BattleListManager:restoreInstancesState()
    local instancesData = g_settings.getNode('BattleListInstances')
    if not instancesData then return end

    local hasExistingInstances = false
    for id, instance in pairs(self.instances) do
        if id ~= 0 then
            hasExistingInstances = true
            break
        end
    end

    if hasExistingInstances then
        g_settings.mergeNode('BattleListInstances', {})
        return
    end

    self.isRestoring = true
    for id, data in pairs(instancesData) do
        if tonumber(id) and tonumber(id) > 0 then
            if data.isOpen then
                local oldSettingsKey = 'BattleList_' .. id
                local oldSettings = g_settings.getNode(oldSettingsKey)
                local shouldRestoreSettings = oldSettings ~= nil

                local customName = data.name and data.name ~= tr('Battle List') and data.name or nil
                local instance = self:createNewInstance(customName)

                if instance and instance.window then
                    if shouldRestoreSettings and oldSettings then
                        local newSettingsKey = instance:getSettingsKey()
                        local settingsToRestore = table.copy(oldSettings)
                        settingsToRestore.customName = data.name

                        g_settings.mergeNode(newSettingsKey, settingsToRestore)

                        instance.name = data.name
                        instance:updateTitle()

                        scheduleEvent(function()
                            instance:loadHideButtonStates()
                            instance:loadFilters()
                            instance:loadLockState()
                        end, 50)
                    end

                    if data.windowPos then
                        instance.window:setPosition(data.windowPos)
                    end
                    if data.windowSize then
                        instance.window:setSize(data.windowSize)
                    end

                    if data.isLocked then
                        instance.window:lock(true)
                    else
                        instance.window:unlock(true)
                    end

                    -- Restore minimized state if window was minimized
                    if data.isMinimized then
                        scheduleEvent(function()
                            instance.window:minimize(true)
                        end, 150)
                    end

                    scheduleEvent(function()
                        if not instance.filterPanel.originalHeight then
                            instance.filterPanel.originalHeight = instance.filterPanel:getHeight()
                        end

                        if data.isHidingFilters then
                            instance:hideFilterPanel()
                        else
                            instance:showFilterPanel()
                        end
                    end, 200)

                    g_settings.set(oldSettingsKey, nil)
                end
            else
                local oldSettingsKey = 'BattleList_' .. id
                g_settings.set(oldSettingsKey, nil)
            end
        end
    end

    local allSettings = g_settings.getNode() or {}
    for key, _ in pairs(allSettings) do
        if type(key) == 'string' and key:match('^BattleList_%d+$') then
            local instanceId = tonumber(key:match('%d+'))
            if instanceId and instanceId > 0 then
                g_settings.set(key, nil)
            end
        end
    end

    g_settings.mergeNode('BattleListInstances', {})
    self.isRestoring = false
end

function BattleListManager:getMainInstance()
    return self.instances[0]
end

function BattleListManager:createNewInstance(customName)
    local instance = BattleListInstance:new(self.nextId, customName)
    self.instances[self.nextId] = instance
    self.nextId = self.nextId + 1
    self:createWindowForInstance(instance)
    return instance
end

function BattleListManager:createWindowForInstance(instance)
    local newWindow = g_ui.loadUI('battle')
    instance.window = newWindow
    newWindow:setId('battleWindow_' .. instance.id)

    -- Define resize restriction function
    local function restrictResize()
        local originalOnResize = newWindow.onResize
        newWindow.onResize = function(...)
            if originalOnResize then
                originalOnResize(...)
            end

            local minHeight = 80
            if not instance:isHidingFilters() then
                minHeight = minHeight + 60
            end
            if newWindow:getHeight() < minHeight then
                newWindow:setHeight(minHeight)
            end
        end
    end

    -- Change icon for battle list instances (not the main battle list)
    if instance.id ~= 0 then
        local miniwindowIcon = newWindow:recursiveGetChildById('miniwindowIcon')
        if miniwindowIcon then
            miniwindowIcon:setImageSource('/images/game/battle/icon-battlelist-secondary-widget')
        end
    end

    instance.panel = newWindow:recursiveGetChildById('battlePanel')
    instance.filterPanel = newWindow:recursiveGetChildById('filterPanel')
    instance.toggleFilterButton = newWindow:recursiveGetChildById('toggleFilterButton')

    -- Setup scrollbar for this instance - use default MiniWindow behavior
    local scrollbar = newWindow:getChildById('miniwindowScrollBar')
    if scrollbar then
        scrollbar:mergeStyle({ ['$!on'] = {} })
    end

    if not instance.toggleFilterButton then
        g_logger.info("Battle: toggleFilterButton not found in battle instance " .. instance.id .. " UI")
    end

    instance:updateTitle()

    local hideButtons = {}
    -- local options = { 'hidePlayers', 'hideNPCs', 'hideMonsters', 'hideSkulls', 'hideParty', 'hideKnights', 'hidePaladins', 'hideDruids', 'hideSorcerers', 'hideMonks', 'hideSummons', 'hideMembersOwnGuild' }
    local options = { 'hidePlayers', 'hideNPCs', 'hideMonsters', 'hideSkulls', 'hideParty', 'hideMembersOwnGuild' }
    for i, v in ipairs(options) do
        hideButtons[v] = newWindow:recursiveGetChildById(v)
        if hideButtons[v] then
            hideButtons[v].onClick = function(button)
                instance:onFilterButtonClick(button)
            end
        end
    end
    instance.hideButtons = hideButtons

    instance:loadHideButtonStates()

    local contextMenuButton = newWindow:recursiveGetChildById('contextMenuButton')
    if contextMenuButton then
        contextMenuButton.onClick = function(widget, mousePos, mouseButton)
            return instance:showContextMenu(widget, mousePos, mouseButton)
        end
    end

    local newWindowButton = newWindow:recursiveGetChildById('newWindowButton')
    if newWindowButton then
        newWindowButton.onClick = function()
            BattleListManager:createNewInstance()
        end
    end

    if instance.toggleFilterButton then
        instance.toggleFilterButton.onClick = function()
            instance:toggleFilterPanel()
            restrictResize()

            local minHeight = 80
            if not instance:isHidingFilters() then
                minHeight = minHeight + 60
            end
            if newWindow:getHeight() < minHeight then
                newWindow:setHeight(minHeight)
            end
        end
    end

    newWindow.onOpen = function()
        instance:onOpen()
    end

    newWindow.onClose = function()
        instance:onClose()
        if instance.id ~= 0 then
            instance:destroy(false)
        end
    end

    newWindow.onGeometryChange = function()
        if instance.id ~= 0 and not BattleListManager.isRestoring then
            BattleListManager:saveInstancesState()
        end
    end

    local lockButton = newWindow:getChildById('lockButton')
    if lockButton then
        local originalOnClick = lockButton.onClick
        lockButton.onClick = function()
            if originalOnClick then
                originalOnClick()
            end
            if not BattleListManager.isRestoring then
                scheduleEvent(function()
                    instance:saveLockState()
                    if instance.id ~= 0 then
                        BattleListManager:saveInstancesState()
                    end
                end, 10)
            end
        end
    end

    newWindow.onMousePress = function(widget, mousePos, button)
        if button == MouseRightButton then
            local menu = g_ui.createWidget('PopupMenu')
            menu:addOption('Edit Name', function() instance:openEditNameDialog() end)
            menu:addOption('Create New Battle List', function() BattleListManager:createNewInstance() end)
            if instance.id ~= 0 then
                menu:addOption('Clear Configurations', function() instance:clearAllConfigurations() end)
                menu:addOption('Close Battle List', function() instance:destroy(false) end)
            end
            menu:display(mousePos)
            return true
        end
        return false
    end

    newWindow:setContentMinimumHeight(80)

    restrictResize()

    local originalOnMinimize = newWindow.onMinimize
    local originalOnMaximize = newWindow.onMaximize

    newWindow.onMinimize = function(...)
        if originalOnMinimize then originalOnMinimize(...) end
        newWindow.onResize = nil
    end

    newWindow.onMaximize = function(...)
        if originalOnMaximize then originalOnMaximize(...) end
        restrictResize()
    end

    newWindow:setup()

    local panel = modules.game_interface.findContentPanelAvailable(newWindow, newWindow:getMinimumHeight())
    if panel then
        panel:addChild(newWindow)
        newWindow:open()
    end

    -- Set initial scrollbar position for new instances (filters visible by default)
    local scrollbar = newWindow:getChildById('miniwindowScrollBar')
    if scrollbar then
        -- Header (16px) + filterPanel height (46px) + spacing panel (18px) + separator (2px)
        scrollbar:setMarginTop(82) -- 16 + 46 + 18 + 2 = 82
    end

    if g_game.isOnline() then
        instance:checkCreatures()
    end
end

function BattleListManager:getMainInstance()
    return self.instances[0]
end

function BattleListManager:getInstance(id)
    return self.instances[id]
end

function BattleListManager:getAllInstances()
    return self.instances
end

function BattleListManager:destroyInstance(id)
    local instance = self.instances[id]
    if instance then
        instance:destroy(false)
        self:removeSavedInstanceState(id)
    end
end

function BattleListManager:removeSavedInstanceState(id)
    local instancesData = g_settings.getNode('BattleListInstances') or {}
    if instancesData[tostring(id)] then
        instancesData[tostring(id)] = nil
        g_settings.mergeNode('BattleListInstances', instancesData)
    end
end

function BattleListManager:startPeriodicSave()
    self:stopPeriodicSave()
    self.autoSaveEvent = scheduleEvent(function()
        if g_game.isOnline() and not self.isRestoring then
            self:saveInstancesState()
            self:startPeriodicSave()
        end
    end, 30000)
end

function BattleListManager:stopPeriodicSave()
    if self.autoSaveEvent then
        removeEvent(self.autoSaveEvent)
        self.autoSaveEvent = nil
    end
end

-- Battle List Instance Class
BattleListInstance = {
    id = nil,
    window = nil,
    panel = nil,
    filterPanel = nil,
    toggleFilterButton = nil,
    binaryTree = {},
    battleButtons = {},
    lastBattleButtonSwitched = nil,
    settings = {},
    name = "Battle List"
}

function BattleListInstance:new(id, customName)
    local instance = {}
    setmetatable(instance, {__index = self})

    instance.id = id or BattleListManager.nextId
    instance.binaryTree = {}
    instance.battleButtons = {}
    instance.lastBattleButtonSwitched = nil
    instance.lastAge = 0
    instance.name = customName or tr('Battle List')
    instance.settings = {
        filters = table.copy(BATTLE_FILTERS),
        sortType = 'name',
        sortOrder = 'A',
        hidingFilters = false,
        customName = instance.name
    }

    return instance
end

function BattleListInstance:getSettingsKey()
    return 'BattleList_' .. self.id
end

function BattleListInstance:loadFilters()
    local settings = g_settings.getNode(self:getSettingsKey())
    if not settings or not settings['filters'] then
        return table.copy(BATTLE_FILTERS)
    end
    return settings['filters']
end

function BattleListInstance:saveFilters()
    local currentFilters = self:loadFilters()
    g_settings.mergeNode(self:getSettingsKey(), { ['filters'] = currentFilters })
end

function BattleListInstance:saveHideButtonStates()
    if not self.hideButtons then return end

    local hideButtonStates = {}
    for buttonName, button in pairs(self.hideButtons) do
        if button then
            hideButtonStates[buttonName] = button:isChecked()
        end
    end
    g_settings.mergeNode(self:getSettingsKey(), { ['hideButtons'] = hideButtonStates })
end

function BattleListInstance:saveLockState()
    if self.window then
        local isLocked = self.window:getSettings('locked') or false
        local lockButton = self.window:getChildById('lockButton')
        if lockButton then
            isLocked = lockButton:isOn()
        end
        g_settings.mergeNode(self:getSettingsKey(), { ['isLocked'] = isLocked })
    end
end

function BattleListInstance:loadLockState()
    if not self.window then
        return false
    end

    local settings = g_settings.getNode(self:getSettingsKey())
    if settings and settings['isLocked'] ~= nil then
        local isLocked = settings['isLocked']
        local lockButton = self.window:getChildById('lockButton')
        if isLocked then
            self.window:lock(true)
            if lockButton then
                lockButton:setOn(true)
            end
        else
            self.window:unlock(true)
            if lockButton then
                lockButton:setOn(false)
            end
        end
        return isLocked
    end
    return false
end
function BattleListInstance:loadHideButtonStates()
    if not self.hideButtons then return end

    local settings = g_settings.getNode(self:getSettingsKey())
    if settings and settings['hideButtons'] then
        for buttonName, isChecked in pairs(settings['hideButtons']) do
            local button = self.hideButtons[buttonName]
            if button then
                button:setChecked(isChecked)
            end
        end
    end
end

function BattleListInstance:getFilter(filter)
    local filters = self:loadFilters()
    local value = filters[filter]
    if value ~= nil then
        return value
    end
    return BATTLE_FILTERS[filter] or false
end

function BattleListInstance:setFilter(filter)
    local filters = self:loadFilters()
    local value = filters[filter]

    if value == nil then
        value = BATTLE_FILTERS[filter]
        if value == nil then
            return false
        end
    end

    if filter:find("sortAscBy") or filter:find("sortDescBy") then
        for filterName, _ in pairs(BATTLE_FILTERS) do
            if filterName ~= filter and (filterName:find("sortAscBy") or filterName:find("sortDescBy")) then
                filters[filterName] = false
            end
        end
    end

    filters[filter] = not value
    g_settings.mergeNode(self:getSettingsKey(), { ['filters'] = filters })

    scheduleEvent(function()
        self:checkCreatures()
    end, 50)

    return true
end

function BattleListInstance:getSortType()
    local filters = self:loadFilters()

    for filterName, isActive in pairs(filters) do
        if isActive and (filterName:find("sortAscBy") or filterName:find("sortDescBy")) then
            if filterName:find("DisplayTime") then
                return 'age'
            elseif filterName:find("Distance") then
                return 'distance'
            elseif filterName:find("HitPoints") then
                return 'health'
            elseif filterName:find("Name") then
                return 'name'
            end
        end
    end

    return 'name'
end

function BattleListInstance:setSortType(state, oldSortType)
    g_settings.mergeNode(self:getSettingsKey(), { ['sortType'] = state })
    local order = self:getSortOrder()
    self:reSort(oldSortType, state, order, order)
end

function BattleListInstance:getSortOrder()
    local filters = self:loadFilters()

    for filterName, isActive in pairs(filters) do
        if isActive and (filterName:find("sortAscBy") or filterName:find("sortDescBy")) then
            return filterName:find("sortAscBy") and 'A' or 'D'
        end
    end

    return 'A'
end

function BattleListInstance:setSortOrder(state, oldSortOrder)
    g_settings.mergeNode(self:getSettingsKey(), { ['sortOrder'] = state })
    self:reSort(false, false, oldSortOrder, state)
end

function BattleListInstance:isSortAsc()
    return self:getSortOrder() == 'A'
end

function BattleListInstance:isSortDesc()
    return self:getSortOrder() == 'D'
end

function BattleListInstance:getName()
    local settings = g_settings.getNode(self:getSettingsKey())
    if settings and settings['customName'] then
        return settings['customName']
    end
    return tr('Battle List')
end

function BattleListInstance:setName(name)
    local settings = g_settings.getNode(self:getSettingsKey()) or {}
    settings['customName'] = (name == nil or name == '') and tr('Battle List') or name
    g_settings.mergeNode(self:getSettingsKey(), settings)
    self:updateTitle()

    if self.id ~= 0 and not BattleListManager.isRestoring then
        BattleListManager:saveInstancesState()
    end
end

function BattleListInstance:updateTitle()
    if self.window then
        local titleLabel = self.window:recursiveGetChildById('miniwindowTitle')
        if titleLabel then
            local title = self:getName()
            -- Limit title to 11 characters, replace last 3 with "..." if longer
            if string.len(title) > 11 then
                title = string.sub(title, 1, 8) .. "..."
            end
            titleLabel:setText(title)
        end
    end
end

function BattleListInstance:clearAllConfigurations()
    local settingsKey = self:getSettingsKey()
    local settings = g_settings.getNode(settingsKey)
    if settings then
        g_settings.mergeNode(settingsKey, {
            filters = nil,
            sortType = nil,
            sortOrder = nil,
            hidingFilters = nil,
            customName = nil,
            hideButtons = nil,
            isLocked = nil
        })
    end

    self.settings = {
        filters = table.copy(BATTLE_FILTERS),
        sortType = 'name',
        sortOrder = 'A',
        hidingFilters = false,
        customName = tr('Battle List')
    }

    if self.hideButtons then
        for _, button in pairs(self.hideButtons) do
            if button then
                button:setChecked(false)
            end
        end
    end

    self:updateTitle()

    if self.filterPanel and not self.filterPanel:isVisible() then
        self:showFilterPanel()
    end

    if self.window then
        self.window:unlock()
    end

    self:checkCreatures()
end

function BattleListInstance:destroy(saveSettings)
    if not saveSettings then
        local settingsKey = self:getSettingsKey()
        local settings = g_settings.getNode(settingsKey)
        if settings then
            g_settings.mergeNode(settingsKey, {
                filters = nil,
                sortType = nil,
                sortOrder = nil,
                hidingFilters = nil,
                customName = nil,
                hideButtons = nil,
                isLocked = nil
            })
        end

        if self.id ~= 0 then
            BattleListManager:removeSavedInstanceState(self.id)
        end
    else
        self:saveFilters()
        self:saveHideButtonStates()
        self:saveLockState()
    end

    for i, v in pairs(self.battleButtons) do
        BattleButtonPool:release(v)
    end

    self.binaryTree = {}
    self.battleButtons = {}
    self.settings = nil
    self.lastBattleButtonSwitched = nil

    if self.window and self.id ~= 0 then
        self.window:destroy()
        self.window = nil
    end

    self.panel = nil
    self.filterPanel = nil
    self.toggleFilterButton = nil
    self.hideButtons = nil

    BattleListManager.instances[self.id] = nil
end

-- Instance-specific methods
function BattleListInstance:onFilterButtonClick(button)
    button:setChecked(not button:isChecked())
    self:saveHideButtonStates()
    self:checkCreatures()
end

function BattleListInstance:showContextMenu(widget, mousePos, mouseButton)
    local menu = g_ui.createWidget('BattleListSubMenu')
    menu:setGameMenu(true)
    for _, choice in ipairs(menu:getChildren()) do
        local choiceId = choice:getId()
        if choiceId and choiceId ~= 'HorizontalSeparator' then
            if choiceId == 'editBattleListName' or choiceId == 'openNewBattleList' then
                choice.onClick = function()
                    self:onMenuAction(choiceId)
                    menu:destroy()
                end
            else
                local filterValue = self:getFilter(choiceId)
                choice:setChecked(filterValue)
                choice.onCheckChange = function()
                    self:onMenuAction(choiceId)
                    menu:destroy()
                end
            end
        end
    end

    local buttonPos = widget:getPosition()
    local buttonSize = widget:getSize()
    local menuWidth = menu:getWidth()

    local buttonCenterX = buttonPos.x + buttonSize.width / 2
    local buttonCenterY = buttonPos.y + buttonSize.height / 2

    local menuX = buttonCenterX - menuWidth
    local menuY = buttonCenterY

    menu:display({x = menuX, y = menuY})
    return true
end

function BattleListInstance:onMenuAction(actionId)
    if actionId == 'editBattleListName' then
        self:openEditNameDialog()
    elseif actionId == 'openNewBattleList' then
        BattleListManager:createNewInstance()
    else
        self:setFilter(actionId)
    end
end

function BattleListInstance:openEditNameDialog()
    local currentName = self:getName()
    if currentName == tr('Battle List') then
        currentName = ""
    end

    local changeListNameWindow = g_ui.displayUI('style/changeListName')
    changeListNameWindow:show()

    local nameInput = changeListNameWindow:getChildById('newBattleListName')
    nameInput:setText(currentName)
    nameInput:focus()
    nameInput:selectAll()

    local function closeWindow()
        nameInput:setText('')
        changeListNameWindow:setVisible(false)
        changeListNameWindow:destroy()
    end

    changeListNameWindow.buttonOk.onClick = function()
        local newName = nameInput:getText()
        self:setName(newName)
        closeWindow()
    end

    changeListNameWindow.closeButton.onClick = closeWindow
    changeListNameWindow.onEscape = closeWindow

    nameInput.onKeyDown = function(widget, keyCode, keyboardModifiers)
        if keyCode == KeyReturn or keyCode == KeyEnter then
            changeListNameWindow.buttonOk.onClick()
            return true
        elseif keyCode == KeyEscape then
            closeWindow()
            return true
        end
        return false
    end
end

function BattleListInstance:toggleFilterPanel()
    if self.filterPanel:isVisible() then
        self:hideFilterPanel()
    else
        self:showFilterPanel()
    end
end

function BattleListInstance:hideFilterPanel()
    self.filterPanel.originalHeight = self.filterPanel:getHeight()
    self.filterPanel:setHeight(0)
    if self.toggleFilterButton then
        self.toggleFilterButton:getParent():setMarginTop(0)
        self.toggleFilterButton:setOn(false)
    end
    self:setHidingFilters(true)
    local HorizontalSeparator = self.window:recursiveGetChildById('HorizontalSeparator')
    if HorizontalSeparator then HorizontalSeparator:setVisible(false) end
    self.filterPanel:setVisible(false)

    -- Adjust spacing panel height when filters are hidden
    local filterSpacingPanel = self.window:recursiveGetChildById('filterSpacingPanel')
    if filterSpacingPanel then filterSpacingPanel:setHeight(18) end

    local contentsPanel = self.window:recursiveGetChildById('contentsPanel')
    -- Reduce margin by the full filter panel space: 46px (filter) + 18px (spacing) + 2px (separator) + 6px (margin) = 72px
    if contentsPanel then contentsPanel:setMarginTop(-23) end

    -- Adjust scrollbar to start at header bottom when filter is hidden
    local scrollbar = self.window:getChildById('miniwindowScrollBar')
    if scrollbar then
        scrollbar:setMarginTop(16) -- Default header height
    end

    -- Update resize restrictions
    if self.window.onResize then
        local function restrictResize()
            self.window.onResize = function()
                local minHeight = 80 -- Base minimum height
                if not self:isHidingFilters() then
                    minHeight = minHeight + 60 -- Add extra height for visible filters
                end
                if self.window:getHeight() < minHeight then
                    self.window:setHeight(minHeight)
                end
            end
        end
        restrictResize()
    end
end

function BattleListInstance:showFilterPanel()
    if self.toggleFilterButton then
        self.toggleFilterButton:getParent():setMarginTop()
        self.toggleFilterButton:setOn(true)
    end
    -- Ensure originalHeight is set, fallback to a default height
    if not self.filterPanel.originalHeight then
        self.filterPanel.originalHeight = 40  -- Default filter panel height
    end
    self.filterPanel:setHeight(self.filterPanel.originalHeight)
    self:setHidingFilters(false)
    local HorizontalSeparator = self.window:recursiveGetChildById('HorizontalSeparator')
    if HorizontalSeparator then HorizontalSeparator:setVisible(true) end
    self.filterPanel:setVisible(true)

    -- Adjust spacing panel height when filters are visible
    local filterSpacingPanel = self.window:recursiveGetChildById('filterSpacingPanel')
    if filterSpacingPanel then filterSpacingPanel:setHeight(2) end

    local contentsPanel = self.window:recursiveGetChildById('contentsPanel')
    if contentsPanel then contentsPanel:setMarginTop(0) end

    -- Adjust scrollbar to start at filter panel bottom when filter is visible
    local scrollbar = self.window:getChildById('miniwindowScrollBar')
    if scrollbar then
        -- Header (16px) + filterPanel height + spacing panel (2px) + separator (2px)
        local totalMargin = 16 + self.filterPanel.originalHeight + 2 + 2
        scrollbar:setMarginTop(totalMargin)
    end

    -- Update resize restrictions
    if self.window.onResize then
        local function restrictResize()
            self.window.onResize = function()
                local minHeight = 80 -- Base minimum height
                if not self:isHidingFilters() then
                    minHeight = minHeight + 60 -- Add extra height for visible filters
                end
                if self.window:getHeight() < minHeight then
                    self.window:setHeight(minHeight)
                end
            end
        end
        restrictResize()
    end
end

function BattleListInstance:setHidingFilters(state)
    local settings = {}
    settings['hidingFilters'] = state
    g_settings.mergeNode(self:getSettingsKey(), settings)
end

function BattleListInstance:isHidingFilters()
    local settings = g_settings.getNode(self:getSettingsKey())
    if not settings then
        return false
    end
    return settings['hidingFilters']
end

function BattleListInstance:onOpen()
    -- Ensure events are connected when opening any battle list instance
    if g_game.isOnline() then
        connecting()
    end

    -- Ensure default filters are applied for new instances
    local filters = self:loadFilters()
    local hasAnySortFilter = false

    -- Check if any sort filter is already active
    for filterName, isActive in pairs(filters) do
        if isActive and (filterName:find("sortAscBy") or filterName:find("sortDescBy")) then
            hasAnySortFilter = true
            break
        end
    end

    -- If no sort filter is active, apply the default
    if not hasAnySortFilter then
        filters["sortAscByDisplayTime"] = true
        g_settings.mergeNode(self:getSettingsKey(), { ['filters'] = filters })
        scheduleEvent(function()
            self:checkCreatures()
        end, 100)
    end
end

function BattleListInstance:onClose()
    -- Don't clear instance configurations when window is closed during normal operation
    -- Only clear when explicitly requested through clearAllConfigurations()

    -- Check if we need to disconnect global events when this instance closes
    scheduleEvent(function()
        -- Check if main battle list is closed and no other instances are open
        local mainInstance = BattleListManager.instances[0]
        local isMainClosed = not mainInstance or not mainInstance.window or not mainInstance.window:isVisible()

        if isMainClosed then
            local hasOpenInstances = false
            for id, instance in pairs(BattleListManager.instances) do
                if id ~= 0 and instance.window and instance.window:isVisible() then
                    hasOpenInstances = true
                    break
                end
            end

            if not hasOpenInstances then
                disconnecting()
            end
        end
    end, 10) -- Small delay to ensure window state is updated
end

function BattleListInstance:checkCreatures()
    if not self.panel or not g_game.isOnline() then
        return false
    end

    self.panel:disableUpdateTemporarily()

    local player = g_game.getLocalPlayer()
    if not player then
        return false
    end

    local position = player:getPosition()
    if not position then
        return false
    end

    self:removeAllCreatures()

    local spectators = modules.game_interface.getMapPanel():getSpectators()
    local sortType = self:getSortType()
    local sortOrder = self:getSortOrder()

    for _, creature in ipairs(spectators) do
        if self:doCreatureFitFilters(creature) then
            self:addCreature(creature, sortType)
        end
    end
end

function BattleListInstance:doCreatureFitFilters(creature)
    if creature:isLocalPlayer() then
        return false
    end

    if creature:isDead() then
        return false
    end

    local pos = creature:getPosition()
    if not pos then
        return false
    end

    local localPlayer = g_game.getLocalPlayer()
    if not localPlayer then
        return false
    end

    local position = localPlayer:getPosition()
    if not position then
        return false
    end

    if pos.z ~= localPlayer:getPosition().z or not creature:canBeSeen() then
        return false
    end

    for i, v in pairs(self.hideButtons or {}) do
        if v:isChecked() then
            if (i == 'hidePlayers' and creature:isPlayer()) or
               (i == 'hideNPCs' and creature:isNpc()) or
               (i == 'hideMonsters' and creature:isMonster()) or
               (i == 'hideSkulls' and (creature:isPlayer() and creature:getSkull() == SkullNone)) or
               (i == 'hideParty' and creature:isPlayer() and (function()
                   local shield = creature:getShield()
                   return shield and (shield == ShieldYellow or
                                    shield == ShieldYellowSharedExp or
                                    shield == ShieldYellowNoSharedExp or
                                    shield == ShieldBlue or
                                    shield == ShieldBlueNoSharedExpBlink or
                                    shield == ShieldBlueSharedExp or
                                    shield == ShieldYellowNoSharedExpBlink)
               end)()) or
               (i == 'hideKnights' and creature:isPlayer() and creature:isKnight()) or
               (i == 'hidePaladins' and creature:isPlayer() and creature:isPaladin()) or
               (i == 'hideDruids' and creature:isPlayer() and creature:isDruid()) or
               (i == 'hideSorcerers' and creature:isPlayer() and creature:isSorcerer()) or
               (i == 'hideMonks' and creature:isPlayer() and creature:isMonk()) or
               (i == 'hideSummons' and creature:isMonster() and (function()
                   local masterId = creature:getMasterId()
                   return masterId and masterId > 0
               end)()) or
               (i == 'hideMembersOwnGuild' and creature:isPlayer() and creature:getEmblem() == localPlayer:getEmblem() and creature:getEmblem() ~= EmblemNone) then
                return false
            end
        end
    end

    return true
end

local lastAge = 0
function BattleListInstance:addCreature(creature, sortType)
    local creatureId = creature:getId()
    local battleButton = self.battleButtons[creatureId]
    if battleButton then
        -- Safety check: don't update if creature is nil
        if battleButton.creature then
            battleButton:update()
        end
    else
        if creature:getPosition() == nil then
            return
        end

        local newCreature = {}
        newCreature.id = creatureId
        newCreature.name = creature:getName():lower()
        -- Clamp to valid range; when a creature first appears client-side, health can be 101 ("unknown")
        -- Using 101 in the sorter breaks binary search logic; clamp to 100 for deterministic ordering
        local hp = creature:getHealthPercent()
        newCreature.healthpercent = math.max(0, math.min(100, hp or 0))
        newCreature.distance = getDistanceBetween(g_game.getLocalPlayer():getPosition(), creature:getPosition())
        newCreature.age = self.lastAge + 1
        self.lastAge = self.lastAge + 1

        local newIndex = binaryInsert(self.binaryTree, newCreature, BSComparatorSortType, sortType, true)

        battleButton = BattleButtonPool:get()
        battleButton:setup(creature, true)

        battleButton.data = {}
        for i, v in pairs(newCreature) do
            battleButton.data[i] = v
        end

        self.battleButtons[creatureId] = battleButton

        if creature == g_game.getAttackingCreature() then
            battleButton.isTarget = true
            if battleButton.creature then
                battleButton:update()
            end
        end

        if creature == g_game.getFollowingCreature() then
            battleButton.isFollowed = true
            if battleButton.creature then
                battleButton:update()
            end
        end

        if self:isSortAsc() then
            self.panel:insertChild(newIndex, battleButton)
        else
            self.panel:insertChild((#self.binaryTree - newIndex + 1), battleButton)
        end
    end

    battleButton:setVisible(canBeSeen(creature))
    self.panel:getLayout():update()
end

function BattleListInstance:removeAllCreatures()
    self:removeCreature(false, true)
end

function BattleListInstance:removeCreature(creature, all)
    if all then
        self.binaryTree = {}
        self.lastBattleButtonSwitched = nil
        for i, v in pairs(self.battleButtons) do
            BattleButtonPool:release(v)
        end
        self.battleButtons = {}
        return true
    end

    local creatureId = creature:getId()
    local battleButton = self.battleButtons[creatureId]

    if battleButton then
        if self.lastBattleButtonSwitched == battleButton then
            self.lastBattleButtonSwitched = nil
        end

        local sortType = self:getSortType()
        local valuetoSearch = self:getAttributeByOrderType(battleButton, sortType)
        assert(valuetoSearch, 'Could not find information (data) in sent battleButton')
        valuetoSearch.id = creatureId

        local index = binarySearch(self.binaryTree, valuetoSearch, BSComparatorSortType, sortType, creatureId)
        if index ~= nil and creatureId == self.binaryTree[index].id then
            local creatureListSize = #self.binaryTree
            if index < creatureListSize then
                for i = index, creatureListSize - 1 do
                    -- Swap elements in instance binary tree
                    local tmp = self.binaryTree[i]
                    self.binaryTree[i] = self.binaryTree[i + 1]
                    self.binaryTree[i + 1] = tmp
                end
            end
            self.binaryTree[creatureListSize] = nil
            BattleButtonPool:release(battleButton)
            self.battleButtons[creatureId] = nil
            return true
        end
    end
    return false
end

function BattleListInstance:getAttributeByOrderType(battleButton, orderType)
    if battleButton.data then
        local battleButton = battleButton.data
        if orderType == 'distance' then
            return {
                distance = battleButton.distance
            }
        elseif orderType == 'health' then
            return {
                healthpercent = battleButton.healthpercent
            }
        elseif orderType == 'age' then
            return {
                age = battleButton.age
            }
        else
            return {
                name = battleButton.name
            }
        end
    end
    return false
end

function BattleListInstance:correctBattleButtons(sortOrder)
    self.panel:disableUpdateTemporarily()

    local sortOrder = sortOrder or self:getSortOrder()

    local start = sortOrder == 'A' and 1 or #self.binaryTree
    local finish = #self.binaryTree - start + 1
    local increment = start <= finish and 1 or -1

    local index = 1
    for i = start, finish, increment do
        local v = self.binaryTree[i]
        local battleButton = self.battleButtons[v.id]
        if battleButton ~= nil then
            self.panel:moveChildToIndex(battleButton, index)
            index = index + 1
        end
    end
    return true
end

function BattleListInstance:reSort(oldSortType, newSortType, oldSortOrder, newSortOrder)
    if #self.binaryTree > 1 then
        if newSortType and newSortType ~= oldSortType then
            self:checkCreatures()
        end

        if newSortOrder then
            self:correctBattleButtons(newSortOrder)
        end
    end

    return true
end

function BattleListInstance:swap(index, newIndex) -- Swap indexes of a given table
    local highest = newIndex
    local lowest = index

    if index > newIndex then
        highest = index
        lowest = newIndex
    end

    local tmp = self.binaryTree[lowest]
    self.binaryTree[lowest] = self.binaryTree[highest]
    self.binaryTree[highest] = tmp
end

-- Global legacy functions for backward compatibility
function loadFilters()
    local settings = g_settings.getNode("BattleList")
    if not settings or not settings['filters'] then
        return BATTLE_FILTERS
    end
    return settings['filters']
end

function saveFilters()
    g_settings.mergeNode('BattleList', { ['filters'] = loadFilters() })
end

function getBattleListName()
    local settings = g_settings.getNode('BattleList')
    if settings and settings['customName'] then
        return settings['customName']
    end
    return tr('Battle List')
end

function setBattleListName(name)
    local settings = g_settings.getNode('BattleList') or {}
    settings['customName'] = (name == nil or name == '') and tr('Battle List') or name
    g_settings.mergeNode('BattleList', settings)
    updateBattleListTitle()
end

function updateBattleListTitle()
    if battleWindow then
        local titleLabel = battleWindow:recursiveGetChildById('miniwindowTitle')
        if titleLabel then
            local title = getBattleListName()
            -- Limit title to 11 characters, replace last 3 with "..." if longer
            if string.len(title) > 11 then
                title = string.sub(title, 1, 8) .. "..."
            end
            titleLabel:setText(title)
        end
    end
end

function getFilter(filter)
    local mainInstance = BattleListManager:getMainInstance()
    if mainInstance then
        return mainInstance:getFilter(filter)
    end
    local filters = loadFilters()
    return filters[filter] ~= nil and filters[filter] or (BATTLE_FILTERS[filter] or false)
end

function setFilter(filter)
    local filters = loadFilters()
    local value = filters[filter]

    if value == nil then
        value = BATTLE_FILTERS[filter]
        if value == nil then
            return false
        end
    end

    if filter:find("sortAscBy") or filter:find("sortDescBy") then
        for filterName, _ in pairs(BATTLE_FILTERS) do
            if filterName ~= filter and (filterName:find("sortAscBy") or filterName:find("sortDescBy")) then
                filters[filterName] = false
            end
        end
    end

    filters[filter] = not value
    g_settings.mergeNode('BattleList', { ['filters'] = filters })

    return true
end

-- Game event handlers
function connecting()
    if eventsConnected then
        return true
    end

    connect(LocalPlayer, { onPositionChange = onCreaturePositionChange })
    connect(Creature, {
        onSkullChange = updateCreatureSkull,
        onEmblemChange = updateCreatureEmblem,
        onOutfitChange = onCreatureOutfitChange,
        onHealthPercentChange = onCreatureHealthPercentChange,
        onPositionChange = onCreaturePositionChange,
        onAppear = onCreatureAppear,
        onDisappear = onCreatureDisappear
    })
    connect(UIMap, { onZoomChange = onZoomChange })

    eventsConnected = true

    for _, instance in pairs(BattleListManager.instances) do
        instance:checkCreatures()
    end
    return true
end

function disconnecting(gameEvent)
    if not eventsConnected then
        return true
    end

    disconnect(LocalPlayer, { onPositionChange = onCreaturePositionChange })
    disconnect(Creature, {
        onSkullChange = updateCreatureSkull,
        onEmblemChange = updateCreatureEmblem,
        onOutfitChange = onCreatureOutfitChange,
        onHealthPercentChange = onCreatureHealthPercentChange,
        onPositionChange = onCreaturePositionChange,
        onAppear = onCreatureAppear,
        onDisappear = onCreatureDisappear
    })
    disconnect(UIMap, { onZoomChange = onZoomChange })

    eventsConnected = false
    return true
end

function init()
    -- Initialize Battle Button Pool
    if not ObjectPool then
        BattleButtonPool = {
            get = function()
                local widget = g_ui.createWidget('BattleButton')
                widget:show()
                widget:setOn(true)
                return widget
            end,
            release = function(obj)
                if obj and obj:getParent() then
                    obj:getParent():removeChild(obj)
                end
            end
        }
    else
        BattleButtonPool = ObjectPool.new(
            function()
                local widget = g_ui.createWidget('BattleButton')
                widget:show()
                widget:setOn(true)
                widget.onHoverChange = onBattleButtonHoverChange
                widget.onMouseRelease = onBattleButtonMouseRelease
                return widget
            end,
            function(obj)
                if obj.data then obj.data = nil end

                if lastBattleButtonSwitched == obj then
                    lastBattleButtonSwitched = nil
                end
                if lastCreatureSelected and obj.creature == lastCreatureSelected then
                    lastCreatureSelected = nil
                end

                obj:resetState()

                local parent = obj:getParent()
                if parent then
                    parent:removeChild(obj)
                end
            end)
    end

    g_ui.importStyle('battlebutton')
    battleButton = modules.game_mainpanel.addToggleButton('battleButton', tr('Battle') .. ' (Ctrl+B)',
        '/images/options/button_battlelist', toggle, false, 2)
    battleButton:setOn(true)
    battleWindow = g_ui.loadUI('battle')

    -- Initialize main instance
    BattleListManager.nextId = 1
    local mainInstance = BattleListInstance:new(0, tr('Battle List'))
    mainInstance.window = battleWindow
    mainInstance.panel = battleWindow:recursiveGetChildById('battlePanel')
    mainInstance.filterPanel = battleWindow:recursiveGetChildById('filterPanel')
    mainInstance.toggleFilterButton = battleWindow:recursiveGetChildById('toggleFilterButton')

    if not mainInstance.toggleFilterButton then
        g_logger.info("Battle: toggleFilterButton not found in battle window UI")
    end

    BattleListManager.instances[0] = mainInstance

    -- Store references for backward compatibility
    battlePanel = mainInstance.panel
    filterPanel = mainInstance.filterPanel
    toggleFilterButton = mainInstance.toggleFilterButton

    -- Setup keybind
    Keybind.new("Windows", "Show/hide battle list", "Ctrl+B", "")
    Keybind.bind("Windows", "Show/hide battle list", {{ type = KEY_DOWN, callback = toggle }})

    -- Setup scrollbar - use default MiniWindow behavior
    local scrollbar = battleWindow:getChildById('miniwindowScrollBar')
    if scrollbar then
        scrollbar:mergeStyle({ ['$!on'] = {} })
    end

    HorizontalSeparator = battleWindow:recursiveGetChildById('HorizontalSeparator')
    contentsPanel = battleWindow:recursiveGetChildById('contentsPanel')
    miniwindowScrollBar = battleWindow:getChildById('miniwindowScrollBar')

    -- Setup filter panel
    local settings = g_settings.getNode(mainInstance:getSettingsKey())
    if settings and settings['hidingFilters'] then
        mainInstance:hideFilterPanel()
    else
        -- Set initial scrollbar position when filters are visible
        local scrollbar = battleWindow:getChildById('miniwindowScrollBar')
        if scrollbar then
            -- Header (16px) + filterPanel height (46px) + spacing panel (18px) + separator (2px) + margin (6px)
            scrollbar:setMarginTop(88) -- 16 + 46 + 18 + 2 + 6 = 88
        end
    end

    -- Setup filter buttons
    -- local options = { 'hidePlayers', 'hideNPCs', 'hideMonsters', 'hideSkulls', 'hideParty', 'hideKnights', 'hidePaladins', 'hideDruids', 'hideSorcerers', 'hideMonks', 'hideSummons', 'hideMembersOwnGuild' }
    local options = { 'hidePlayers', 'hideNPCs', 'hideMonsters', 'hideSkulls', 'hideParty', 'hideMembersOwnGuild' }
    for i, v in ipairs(options) do
        hideButtons[v] = battleWindow:recursiveGetChildById(v)
    end

    mainInstance.hideButtons = hideButtons
    mainInstance:loadHideButtonStates()
    mainInstance:loadLockState()

    -- Setup lock button
    local mainLockButton = battleWindow:getChildById('lockButton')
    if mainLockButton then
        local originalOnClick = mainLockButton.onClick
        mainLockButton.onClick = function()
            if originalOnClick then originalOnClick() end
            scheduleEvent(function() mainInstance:saveLockState() end, 10)
        end
    end

    -- Setup mouse widget
    mouseWidget = g_ui.createWidget('UIButton')
    mouseWidget:setVisible(false)
    mouseWidget:setFocusable(false)
    mouseWidget.cancelNextRelease = false

    connect(g_game, {
        onAttackingCreatureChange = onAttack,
        onFollowingCreatureChange = onFollow,
        onGameEnd = onGameEnd,
        onGameStart = onGameStart
    })

    -- Setup context menu
    local contextMenuButton = battleWindow:recursiveGetChildById('contextMenuButton')
    if contextMenuButton then
        contextMenuButton.onClick = function(widget, mousePos, mouseButton)
            return mainInstance:showContextMenu(widget, mousePos, mouseButton)
        end
    end

    local newWindowButton = battleWindow:recursiveGetChildById('newWindowButton')
    if newWindowButton then
        newWindowButton.onClick = function()
            BattleListManager:createNewInstance()
        end
    end

    battleWindow:setContentMinimumHeight(80)

    -- Define resize restriction function
    local function restrictResize()
        local originalOnResize = battleWindow.onResize
        battleWindow.onResize = function(...)
            if originalOnResize then
                originalOnResize(...)
            end

            local minHeight = 80
            if not mainInstance:isHidingFilters() then
                minHeight = minHeight + 60
            end
            if battleWindow:getHeight() < minHeight then
                battleWindow:setHeight(minHeight)
            end
        end
    end

    -- Setup toggleFilterButton onClick handler for main instance
    if mainInstance.toggleFilterButton then
        mainInstance.toggleFilterButton.onClick = function()
            mainInstance:toggleFilterPanel()
            restrictResize()

            local minHeight = 80
            if not mainInstance:isHidingFilters() then
                minHeight = minHeight + 60
            end
            if battleWindow:getHeight() < minHeight then
                battleWindow:setHeight(minHeight)
            end
        end
    end
    restrictResize()

    local originalOnMinimize = battleWindow.onMinimize
    local originalOnMaximize = battleWindow.onMaximize

    battleWindow.onMinimize = function(...)
        if originalOnMinimize then originalOnMinimize(...) end
        battleWindow.onResize = nil
    end

    battleWindow.onMaximize = function(...)
        if originalOnMaximize then originalOnMaximize(...) end
        restrictResize()
    end

    battleWindow:setup()
    updateBattleListTitle()

    battleWindow.onMousePress = function(widget, mousePos, button)
        if button == MouseRightButton then
            local menu = g_ui.createWidget('PopupMenu')
            menu:addOption('Edit Name', function() mainInstance:onMenuAction('editBattleListName') end)
            menu:display(mousePos)
            return true
        end
        return false
    end

    if g_game.isOnline() then
        battleWindow:setupOnStart()
    end
end

-- Binary Search and utility functions
function BSComparator(a, b)
    return a > b and -1 or (a < b and 1 or 0)
end

function BSComparatorSortType(a, b, sortType, id)
    local comparatorA, comparatorB
    if sortType == 'distance' then
        comparatorA, comparatorB = a.distance, (type(b) == 'table' and b.distance or b)
    elseif sortType == 'health' then
        comparatorA, comparatorB = a.healthpercent, type(b) == 'table' and b.healthpercent or b
    elseif sortType == 'age' then
        comparatorA, comparatorB = a.age, type(b) == 'table' and b.age or b
    elseif sortType == 'name' then
        comparatorA, comparatorB = (a.name):lower(), type(b) == 'table' and (b.name):lower() or b
    end

    if comparatorA == nil or comparatorB == nil then
        return 0
    end

    if comparatorA > comparatorB then
        return -1
    elseif comparatorA < comparatorB then
        return 1
    else
        if id and b and b.id then
            return a.id > b.id and -1 or (a.id < b.id and 1 or 0)
        end
        return 0
    end
end

function binarySearch(tbl, value, comparator, ...)
    comparator = comparator or BSComparator
    local mini, maxi = 1, #tbl

    while mini <= maxi do
        local mid = math.floor((maxi + mini) / 2)
        local tmp_value = comparator(tbl[mid], value, ...)

        if tmp_value == 0 then
            return mid
        elseif tmp_value < 0 then
            maxi = mid - 1
        else
            mini = mid + 1
        end
    end
    return nil
end

function binaryInsert(tbl, value, comparator, ...)
    comparator = comparator or BSComparator
    local mini, maxi = 1, #tbl
    local state, mid = 0, 1

    while mini <= maxi do
        mid = math.floor((maxi + mini) / 2)
        if comparator(tbl[mid], value, ...) < 0 then
            maxi, state = mid - 1, 0
        else
            mini, state = mid + 1, 1
        end
    end
    table.insert(tbl, mid + state, value)
    return mid + state
end

function onGameStart()
    battleWindow:setupOnStart() -- load character window configuration

    -- Update battle list title in case it was customized
    updateBattleListTitle()

    -- Load main instance lock state (in case it wasn't loaded during init)
    local mainInstance = BattleListManager.instances[0]
    if mainInstance then
        mainInstance:loadLockState()
    end

    -- Initialize all battle list instances
    for _, instance in pairs(BattleListManager.instances) do
        if instance.window then
            instance.window:setupOnStart()
        end
        instance:updateTitle()
    end

    -- Restore saved battle list instances
    BattleListManager:restoreInstancesState()

    -- Set up periodic auto-save to prevent data loss
    BattleListManager:startPeriodicSave()

    -- Initialize creatures for all instances (including main instance ID 0)
    scheduleEvent(function()
        for _, instance in pairs(BattleListManager.instances) do
            instance:checkCreatures()
        end
    end, 500) -- Increased delay to ensure restored instances are fully set up
end

function onGameEnd()
    -- Stop periodic auto-save
    BattleListManager:stopPeriodicSave()

    battleWindow:setParent(nil, true)
    removeAllCreatures()
    saveFilters()

    -- Save state and clean up all battle list instances without clearing their configurations
    BattleListManager:saveInstancesState() -- This now saves all settings including filters and hide buttons
    for _, instance in pairs(BattleListManager.instances) do
        instance:removeAllCreatures()
        -- Settings are already saved by saveInstancesState(), just clean up UI elements
        if instance.id ~= 0 and instance.window then
            instance.window:setParent(nil, true)
        end
    end

    disconnecting()
end

-- Sort Type Methods
function getSortType() -- Return the current sort type (distance, age, name, health)
    local settings = g_settings.getNode('BattleList')
    if not settings or not settings['sortType'] then
        return 'name'
    end
    return settings['sortType']
end

function setSortType(state, oldSortType) -- Setting the current sort type (distance, age, name, health)
    -- Only update global settings for backwards compatibility
    local settings = {}
    settings['sortType'] = state
    g_settings.mergeNode('BattleList', settings)

    -- Update main instance (ID 0) for backward compatibility, but don't affect other instances
    local mainInstance = BattleListManager.instances[0]
    if mainInstance then
        local order = mainInstance:getSortOrder()
        mainInstance:reSort(oldSortType, state, order, order)
    end
end

function onZoomChange()
    removeEvent(eventOnCheckCreature)
    eventOnCheckCreature = scheduleEvent(function()
        -- Update all battle list instances
        for _, instance in pairs(BattleListManager.instances) do
            instance:checkCreatures()
        end
    end, 1000)
end

function onChangeSortType(comboBox, option) -- Callback when change the sort type (distance, age, name, health)
    local loption = option:lower()
    local oldType = getSortType()

    if loption ~= oldType then
        setSortType(loption, oldType)
    end
end

-- Sort Order Methods
function getSortOrder() -- Return the current sort ordenation (asc/desc)
    local settings = g_settings.getNode('BattleList')
    if not settings then
        return 'A'
    end
    return settings['sortOrder']
end

function setSortOrder(state, oldSortOrder) -- Setting the current sort ordenation (desc/asc)
    -- Only update global settings for backwards compatibility
    local settings = {}
    settings['sortOrder'] = state
    g_settings.mergeNode('BattleList', settings)

    -- Update main instance (ID 0) for backward compatibility, but don't affect other instances
    local mainInstance = BattleListManager.instances[0]
    if mainInstance then
        mainInstance:reSort(false, false, oldSortOrder, state)
    end
end

function isSortAsc() -- Return true if sorted Asc
    return getSortOrder() == 'A'
end

function isSortDesc() -- Return true if sorted Desc
    return getSortOrder() == 'D'
end

function onChangeSortOrder(comboBox, option) -- Callback when change the sort ordenation
    local soption = option:sub(1, 1)
    local oldOrder = getSortOrder()

    if soption ~= oldOrder then
        setSortOrder(option:sub(1, 1), oldOrder)
    end
end

-- Initially checking creatures
function checkCreatures() -- Function that initially populates our tree once the module is initialized
    eventOnCheckCreature = nil

    if not g_game.isOnline() then
        return false
    end

    local player = g_game.getLocalPlayer()
    if not player then
        return false
    end

    local position = player:getPosition()
    if not position then
        return false
    end

    -- Update all battle list instances
    for _, instance in pairs(BattleListManager.instances) do
        if instance.panel then
            instance:checkCreatures()
        end
    end
end

function doCreatureFitFilters(creature) -- Check if creature fit current applied filters (By changing the filter we will call checkCreatures(true) to recreate the tree)
    -- Use main instance for global calls
    local mainInstance = BattleListManager.instances[0]
    if mainInstance then
        return mainInstance:doCreatureFitFilters(creature)
    end
    return false
end

function onFilterButtonClick(button)
    button:setChecked(not button:isChecked())

    -- Save hide button states for main instance
    local mainInstance = BattleListManager.instances[0]
    if mainInstance then
        mainInstance:saveHideButtonStates()
    end

    -- Update all battle list instances
    for _, instance in pairs(BattleListManager.instances) do
        instance:checkCreatures()
    end
end

function canBeSeen(creature)
    return creature and creature:canBeSeen() and creature:getPosition() and
        modules.game_interface.getMapPanel():isInRange(creature:getPosition())
end

function getDistanceBetween(p1, p2) -- Calculate distance
    if p2 == nil then
        p2 = {
            x = 0,
            y = 0
        }
    end

    local xd = math.abs(p1.x - p2.x);
    local yd = math.abs(p1.y - p2.y);

    if xd > 0 then
        xd = xd - 1
    end
    if yd > 0 then
        yd = yd - 1
    end

    return xd + yd
end

-- Adding and Removing creatures
local function getAttributeByOrderType(battleButton, orderType) -- Return the attribute of battleButton based on the orderType
    if battleButton.data then
        local battleButton = battleButton.data
        if orderType == 'distance' then
            return {
                distance = battleButton.distance
            }
        elseif orderType == 'health' then
            return {
                healthpercent = battleButton.healthpercent
            }
        elseif orderType == 'age' then
            return {
                age = battleButton.age
            }
        else
            return {
                name = battleButton.name
            }
        end
    end
    return false
end

local lastAge = 0
function addCreature(creature, sortType) -- Insert a creature in our binary tree
    -- Delegate to main instance
    local mainInstance = BattleListManager.instances[0]
    if mainInstance then
        mainInstance:addCreature(creature, sortType)
    end
end

function removeAllCreatures() -- Remove all creatures from our binary tree
    -- Delegate to main instance
    local mainInstance = BattleListManager.instances[0]
    if mainInstance then
        mainInstance:removeAllCreatures()
    end
end

function removeCreature(creature, all) -- Remove a single creature or all
    -- Delegate to main instance
    local mainInstance = BattleListManager.instances[0]
    if mainInstance then
        return mainInstance:removeCreature(creature, all)
    end
    return false
end

-- Hide/Show Filter Options
function isHidingFilters() -- Return true if filters are hidden
    local settings = g_settings.getNode('BattleList')
    if not settings then
        return false
    end
    return settings['hidingFilters']
end

function setHidingFilters(state) -- Setting hiding filters
    settings = {}
    settings['hidingFilters'] = state
    g_settings.mergeNode('BattleList', settings)
end

function hideFilterPanel() -- Hide Filter panel
    local mainInstance = BattleListManager:getMainInstance()
    if mainInstance then
        mainInstance:hideFilterPanel()
    end
end

function showFilterPanel() -- Show Filter panel
    local mainInstance = BattleListManager:getMainInstance()
    if mainInstance then
        mainInstance:showFilterPanel()
    end
end

function toggleFilterPanel() -- Switching modes of filter panel (hide/show)
    local mainInstance = BattleListManager:getMainInstance()
    if mainInstance then
        mainInstance:toggleFilterPanel()
    end
end

function attackNext(previous)
    local foundTarget = false
    local firstElement = nil
    local lastElement = nil
    local prevElement = nil
    local nextElement = nil

    local mainInstance = BattleListManager.instances[0]
    if not mainInstance or not mainInstance.panel then
        return
    end

    local children = mainInstance.panel:getChildren()

    for _, battleButton in pairs(mainInstance.panel:getChildren()) do
        if battleButton:isVisible() then
            -- select visible first child
            if not firstElement then
                firstElement = battleButton
            end
            lastElement = battleButton

            if battleButton.isTarget then
                foundTarget = true
            elseif foundTarget and not nextElement then
                nextElement = battleButton
            elseif not foundTarget then
                prevElement = battleButton
            end
        end
    end

    if foundTarget then
        if previous then
            if prevElement then
                g_game.attack(prevElement.creature)
            else
                g_game.attack(lastElement.creature)
            end
        else
            if nextElement then
                g_game.attack(nextElement.creature)
            else
                g_game.attack(firstElement.creature)
            end
        end
    elseif firstElement then
        g_game.attack(firstElement.creature)
    else
        return false
    end
    return true
end

-- Connector Callbacks
function onAttack(creature) -- Update battleButton once you're attacking a target
    if lastCreatureSelected then
        lastCreatureSelected:hideStaticSquare()
        lastCreatureSelected = nil
    end

    local foundBattleButton = false

    -- Update all battle list instances
    for _, instance in pairs(BattleListManager.instances) do
        if instance.window and instance.window:isVisible() then
            if creature then
                -- Setting a new target
                local battleButton = instance.battleButtons[creature:getId()]
                if battleButton then
                    battleButton.isTarget = true
                    updateBattleButton(battleButton)
                    foundBattleButton = true
                end
            else
                -- No target (attack cancelled), clear all target flags
                for _, battleButton in pairs(instance.battleButtons) do
                    if battleButton.isTarget then
                        battleButton.isTarget = false
                        updateBattleButton(battleButton)
                    end
                end
            end
        end
    end

    -- If no battle button was found in any instance, show static square on creature
    if not foundBattleButton and creature then
        creature:showStaticSquare(UICreatureButton.getCreatureButtonColors().onTargeted.notHovered)
    end

    lastCreatureSelected = creature
end

function onFollow(creature) -- Update battleButton once you're following a target
    if lastCreatureSelected then
        lastCreatureSelected:hideStaticSquare()
        lastCreatureSelected = nil
    end

    local foundBattleButton = false

    -- Update all battle list instances
    for _, instance in pairs(BattleListManager.instances) do
        if instance.window and instance.window:isVisible() then
            local battleButton = creature and instance.battleButtons[creature:getId()] or nil
            if battleButton then
                battleButton.isFollowed = creature and true or false
                updateBattleButton(battleButton)
                foundBattleButton = true
                -- Don't break here - continue to update other instances
            end
        end
    end

    -- If no battle button was found in any instance, show static square on creature
    if not foundBattleButton and creature then
        creature:showStaticSquare(UICreatureButton.getCreatureButtonColors().onFollowed.notHovered)
    end

    lastCreatureSelected = creature
end

function onCreatureOutfitChange(creature, outfit, oldOutfit) -- Insert/Remove creature when it becomes visible/invisible
    -- Update all battle list instances
    for _, instance in pairs(BattleListManager.instances) do
        local battleButton = instance.battleButtons[creature:getId()]
        local fit = instance:doCreatureFitFilters(creature)

        if battleButton ~= nil and not fit then
            instance:removeCreature(creature)
        elseif battleButton == nil and fit then
            instance:addCreature(creature)
        end
    end
end

function updateCreatureSkull(creature, skullId) -- Update skull
    -- Update all battle list instances
    for _, instance in pairs(BattleListManager.instances) do
        local battleButton = instance.battleButtons[creature:getId()]
        if battleButton then
            battleButton:updateSkull(skullId)
        end
    end
end

function updateCreatureEmblem(creature, emblemId) -- Update emblem
    -- Update all battle list instances
    for _, instance in pairs(BattleListManager.instances) do
        local battleButton = instance.battleButtons[creature:getId()]
        if battleButton then
            battleButton:updateEmblem(emblemId)
        end
    end
end

function onCreaturePositionChange(creature, newPos, oldPos) -- Update battleButton once you or monsters move
    local localPlayer = g_game.getLocalPlayer()
    if not localPlayer then
        return false
    end

    local position = localPlayer:getPosition()
    if not position then
        return false
    end

    -- Update all battle list instances
    for _, instance in pairs(BattleListManager.instances) do
        if instance.panel then
            instance.panel:disableUpdateTemporarily()

            local sortType = instance:getSortType()
            -- If it's the local player moving
            if creature:isLocalPlayer() then
                if oldPos and newPos and newPos.z ~= oldPos.z then
                    addEvent(function() -- fix for old protocols
                        instance:checkCreatures()
                    end)
                elseif oldPos and newPos and (newPos.x ~= oldPos.x or newPos.y ~= oldPos.y) then
                    -- Distance will change when moving, recalculate and move to correct index
                    if #instance.binaryTree > 0 and sortType == 'distance' then
                        -- TODO: If the amount of creatures is higher than a given number, instead of using this approach we simply recalculate each 200ms.
                        for i, v in ipairs(instance.binaryTree) do
                            local oldDistance = v.distance
                            local battleButton = instance.battleButtons[v.id]
                            local mob = battleButton.creature or g_map.getCreatureById(v.id)
                            local newDistance = getDistanceBetween(newPos, mob:getPosition())
                            if oldDistance ~= newDistance then
                                v.distance = newDistance
                                battleButton.data.distance = newDistance
                            end
                        end
                        table.sort(instance.binaryTree, function(a, b)
                            return BSComparatorSortType(a, b, 'distance', true) == 1
                        end)
                        instance:correctBattleButtons()
                    end

                    for i, v in pairs(instance.battleButtons) do
                        local mob = v.creature
                        if mob and mob:getPosition() then
                            v:setVisible(canBeSeen(mob))
                        end
                    end
                end
            else
                -- If it's a creature moving
                local creatureId = creature:getId()
                local battleButton = instance.battleButtons[creatureId]
                local fit = instance:doCreatureFitFilters(creature)

                if battleButton == nil then
                    if fit then
                        instance:addCreature(creature, sortType)
                    end
                else
                    if not fit and newPos then -- if there's no newPos the creature is dead, let onCreatureDisappear handles that.
                        instance:removeCreature(creature)
                    elseif fit then
                        if oldPos and newPos and (newPos.x ~= oldPos.x or newPos.y ~= oldPos.y) then
                            if sortType == 'distance' then
                                local localPlayer = g_game.getLocalPlayer()
                                local newDistance = getDistanceBetween(localPlayer:getPosition(), newPos)
                                local oldDistance = battleButton.data.distance

                                local index = binarySearch(instance.binaryTree, {
                                    distance = oldDistance,
                                    id = creatureId
                                }, BSComparatorSortType, 'distance', true)

                                if index ~= nil and creatureId == instance.binaryTree[index].id then -- Safety first :)
                                    instance.binaryTree[index].distance = newDistance
                                    battleButton.data.distance = newDistance
                                    if newDistance > oldDistance then
                                        if index < #instance.binaryTree then
                                            for i = index, #instance.binaryTree - 1 do
                                                local a = instance.binaryTree[i]
                                                local b = instance.binaryTree[i + 1]
                                                if a.distance > b.distance or (a.distance == b.distance and a.id > b.id) then
                                                    instance:swap(i, i + 1)
                                                end
                                            end
                                        end
                                    elseif newDistance < oldDistance then
                                        battleButton:setVisible(canBeSeen(creature))

                                        if lastCreatureSelected == creature and not battleButton:isVisible() then
                                            lastCreatureSelected:hideStaticSquare()
                                            lastCreatureSelected = nil
                                        end

                                        if index > 1 then
                                            for i = index, 2, -1 do
                                                local a = instance.binaryTree[i - 1]
                                                local b = instance.binaryTree[i]
                                                if a.distance > b.distance or (a.distance == b.distance and a.id > b.id) then
                                                    instance:swap(i - 1, i)
                                                end
                                            end
                                        end
                                    end
                                    instance:correctBattleButtons()
                                else
                                    assert(index ~= nil,
                                        'Not able to update Position Change. Creature: ' .. creature:getName() .. ' id ' ..
                                        creatureId .. ' not found in binary search using ' .. sortType .. ' to find value ' ..
                                        oldDistance .. '.\n')
                                end
                            end
                        end
                        instance:addCreature(creature) -- should check if creature visibility has changed
                    end
                end
            end
        end
    end
end

-- Update battleButton lifebar and, when sorting by health, keep the list ordered
-- Note: previously we only called `battleButton:update()`, which does not touch the lifebar.
-- This caused the HP bar to remain at the value set during creation. We update the bar here.
function onCreatureHealthPercentChange(creature, healthPercent, oldHealthPercent)
    -- Update all battle list instances
    for _, instance in pairs(BattleListManager.instances) do
        local creatureId = creature:getId()
        local battleButton = instance.battleButtons[creatureId]
        if battleButton then
            -- Always refresh the lifebar percent and the cached value, regardless of the current sort type
            -- clamp to [0, 100] just in case
            local pct = math.max(0, math.min(100, healthPercent or 0))
            if battleButton.setLifeBarPercent then
                battleButton:setLifeBarPercent(pct)
            end
            if battleButton.data then
                battleButton.data.healthpercent = pct
            end

            local sortType = instance:getSortType()
            if sortType == 'health' then
                if healthPercent ~= oldHealthPercent and healthPercent ~= 0 then
                    local index = binarySearch(instance.binaryTree, {
                        healthpercent = oldHealthPercent,
                        id = creatureId
                    }, BSComparatorSortType, 'health', true)

                    if index ~= nil and creatureId == instance.binaryTree[index].id then
                        instance.binaryTree[index].healthpercent = healthPercent
                        battleButton.data.healthpercent = healthPercent

                        if healthPercent > oldHealthPercent then
                            if index < #instance.binaryTree then
                                for i = index, #instance.binaryTree - 1 do
                                    local a = instance.binaryTree[i]
                                    local b = instance.binaryTree[i + 1]
                                    if a.healthpercent > b.healthpercent or (a.healthpercent == b.healthpercent and a.id > b.id) then
                                        local tmp = instance.binaryTree[i]
                                        instance.binaryTree[i] = instance.binaryTree[i + 1]
                                        instance.binaryTree[i + 1] = tmp
                                    end
                                end
                            end
                        else
                            if index > 1 then
                                for i = index, 2, -1 do
                                    local a = instance.binaryTree[i - 1]
                                    local b = instance.binaryTree[i]
                                    if a.healthpercent > b.healthpercent or (a.healthpercent == b.healthpercent and a.id > b.id) then
                                        local tmp = instance.binaryTree[i - 1]
                                        instance.binaryTree[i - 1] = instance.binaryTree[i]
                                        instance.binaryTree[i] = tmp
                                    end
                                end
                            end
                        end
                        instance:correctBattleButtons()
                    end
                end
            end

            if battleButton.creature then
                battleButton:update()
            end
        end
    end
end

function onCreatureAppear(creature) -- Update battleButton once a creature appear (add)
    if creature:isLocalPlayer() then
        addEvent(updateStaticSquare)
    end

    -- Update all battle list instances (including main instance ID 0)
    for _, instance in pairs(BattleListManager.instances) do
        local sortType = instance:getSortType()
        if instance:doCreatureFitFilters(creature) then
            instance:addCreature(creature, sortType)
        end
    end
end

function onCreatureDisappear(creature) -- Update battleButton once a creature disappear (remove/dead)
    -- Update all battle list instances (including main instance ID 0)
    for _, instance in pairs(BattleListManager.instances) do
        instance:removeCreature(creature)
    end
end

-- BattleWindow controllers
function onBattleButtonMouseRelease(self, mousePosition, mouseButton) -- Interactions with mouse (right, left, right + left and shift interactions)
    if mouseWidget.cancelNextRelease then
        mouseWidget.cancelNextRelease = false
        return false
    end

    if ((g_mouse.isPressed(MouseLeftButton) and mouseButton == MouseRightButton) or
            (g_mouse.isPressed(MouseRightButton) and mouseButton == MouseLeftButton)) then
        mouseWidget.cancelNextRelease = true
        g_game.look(self.creature, true)
        return true
    elseif mouseButton == MouseLeftButton and g_keyboard.isShiftPressed() then
        g_game.look(self.creature, true)
        return true
    elseif mouseButton == MouseRightButton and not g_mouse.isPressed(MouseLeftButton) then
        modules.game_interface.createThingMenu(mousePosition, nil, nil, self.creature)
        return true
    elseif mouseButton == MouseLeftButton and not g_mouse.isPressed(MouseRightButton) then
        if self.isTarget then
            g_game.cancelAttack()
        else
            g_game.attack(self.creature)
        end
        return true
    end
    return false
end

function updateStaticSquare(battleButton) -- Update all static squares upon appearing the screen (login)
    -- Update all battle list instances
    for _, instance in pairs(BattleListManager.instances) do
        for _, battleButton in pairs(instance.battleButtons) do
            if battleButton.isTarget and battleButton.creature then
                battleButton:update()
            end
        end
    end
end

function updateBattleButton(battleButton) -- Update battleButton with attack/follow squares
    -- Safety check: don't update if creature is nil
    if not battleButton or not battleButton.creature then
        return
    end

    battleButton:update()

    if battleButton.isTarget then
        -- Clear target flag from all other battle buttons representing DIFFERENT creatures in all instances
        local currentCreatureId = battleButton.creature:getId()
        for _, instance in pairs(BattleListManager.instances) do
            for _, otherBattleButton in pairs(instance.battleButtons) do
                if otherBattleButton ~= battleButton and otherBattleButton.isTarget and
                   otherBattleButton.creature and otherBattleButton.creature:getId() ~= currentCreatureId then
                    otherBattleButton.isTarget = false
                    otherBattleButton:update()
                end
            end
        end
        lastBattleButtonSwitched = battleButton
    end

    if battleButton.isFollowed then
        -- Clear follow flag from all other battle buttons representing DIFFERENT creatures in all instances
        local currentCreatureId = battleButton.creature:getId()
        for _, instance in pairs(BattleListManager.instances) do
            for _, otherBattleButton in pairs(instance.battleButtons) do
                if otherBattleButton ~= battleButton and otherBattleButton.isFollowed and
                   otherBattleButton.creature and otherBattleButton.creature:getId() ~= currentCreatureId then
                    otherBattleButton.isFollowed = false
                    otherBattleButton:update()
                end
            end
        end
        lastBattleButtonSwitched = battleButton
    end

    -- Clear lastBattleButtonSwitched if it's a released button
    if lastBattleButtonSwitched and not lastBattleButtonSwitched.creature then
        lastBattleButtonSwitched = nil
    end
end

function onBattleButtonHoverChange(battleButton, hovered) -- Interaction with mouse (hovering)
    if battleButton.isBattleButton then
        battleButton.isHovered = hovered
        updateBattleButton(battleButton)
    end
end

function onOpen()
    battleButton:setOn(true)
    connecting()

    -- Ensure default filters are applied for the main battle list
    local mainInstance = BattleListManager.instances[0]
    if mainInstance then
        local filters = mainInstance:loadFilters()
        local hasAnySortFilter = false

        -- Check if any sort filter is already active
        for filterName, isActive in pairs(filters) do
            if isActive and (filterName:find("sortAscBy") or filterName:find("sortDescBy")) then
                hasAnySortFilter = true
                break
            end
        end

        -- If no sort filter is active, apply the default
        if not hasAnySortFilter then
            filters["sortAscByDisplayTime"] = true
            g_settings.mergeNode(mainInstance:getSettingsKey(), { ['filters'] = filters })
            scheduleEvent(function()
                mainInstance:checkCreatures()
            end, 100)
        end
    end
end

function onClose()
    battleButton:setOn(false)

    -- Only disconnect global events if there are no other battle list instances open
    local hasOpenInstances = false
    for id, instance in pairs(BattleListManager.instances) do
        if id ~= 0 and instance.window and instance.window:isVisible() then
            hasOpenInstances = true
            break
        end
    end

    if not hasOpenInstances then
        disconnecting()
    end
end


function toggle() -- Close/Open the battle window or Pressing Ctrl + B
    if battleButton:isOn() then
        battleWindow:close()
    else
        -- Ensure events are connected when opening the main battle window
        if g_game.isOnline() then
            connecting()
        end

        if not battleWindow:getParent() then
            local panel = modules.game_interface
                .findContentPanelAvailable(battleWindow, battleWindow:getMinimumHeight())
            if not panel then
                return
            end

            panel:addChild(battleWindow)
        end
        battleWindow:open()
    end
end

function terminate() -- Terminating the Module (unload)
    -- Save battle list instances state before destroying
    BattleListManager:saveInstancesState()

    -- Explicitly save main instance settings as well
    local mainInstance = BattleListManager.instances[0]
    if mainInstance then
        mainInstance:saveLockState()
        mainInstance:saveFilters()
        mainInstance:saveHideButtonStates()
    end

    -- Destroy all battle list instances (preserving settings for next session)
    for _, instance in pairs(BattleListManager.instances) do
        instance:destroy(true) -- Preserve settings during module termination
    end
    BattleListManager.instances = {}

    binaryTree = {}
    battleButtons = {}
    hideButtons = {}

    if battleButton then
        battleButton:destroy()
        battleButton = nil
    end

    if battleWindow then
        battleWindow:destroy()
        battleWindow = nil
    end

    if mouseWidget then
        mouseWidget:destroy()
        mouseWidget = nil
    end

    lastCreatureSelected = nil

    battlePanel = nil
    battleButton = nil
    battleWindow = nil
    mouseWidget = nil
    filterPanel = nil
    toggleFilterButton = nil

    Keybind.delete("Windows", "Show/hide battle list")

    disconnect(g_game, {
        onAttackingCreatureChange = onAttack,
        onFollowingCreatureChange = onFollow,
        onGameEnd = onGameEnd,
        onGameStart = onGameStart
    })
    disconnecting()

    -- Reset connection state
    eventsConnected = false
end
