require "ISUI/ISPanel"
require "ISUI/ISButton"
require "ISUI/ISLabel"
require "ISUI/ISTextEntryBox"
require "ISUI/ISModalDialog"
require "MS/MS_PresetManager"
require "MS/SettingPanel/MS_SquareButton"
require "MS/SettingPanel/MS_LongButton"

-- NIScrollView is provided by NeatUI Framework, which must load before Modern Status.

MS_PresetsPanel = ISPanel:derive("MS_PresetsPanel")
MS_PresetsPanel.CurrentPanel = nil

local FONT_HGT_SMALL = getTextManager():getFontHeight(UIFont.Small)
local MAX_PRESET_NAME_LENGTH = 64

local function tr(key, fallback)
    if getTextOrNull then
        local translated = getTextOrNull(key)
        if translated and translated ~= key then
            return translated
        end
    end

    if getText then
        local ok, translated = pcall(getText, key)
        if ok and translated and translated ~= key then
            return translated
        end
    end

    return fallback or key
end

local function trim(value)
    value = tostring(value or "")
    value = string.gsub(value, "^%s+", "")
    value = string.gsub(value, "%s+$", "")
    return value
end

local function isValidPresetName(value)
    local presetName = trim(value)
    if presetName == "" then
        return false, presetName
    end
    if string.len(presetName) > MAX_PRESET_NAME_LENGTH then
        return false, presetName
    end
    if string.find(presetName, "[%c]") then
        return false, presetName
    end
    return true, presetName
end

local function measureButtonWidth(text, minimumWidth, padding)
    local textWidth = getTextManager():MeasureStringX(UIFont.Small, text or "")
    return math.max(minimumWidth or 64, textWidth + (padding or 4) * 4)
end

local function applyTextEntryVerticalCenter(entry)
    -- B42.15+ exposes vertical centering in UITextBox2; older B42 builds do not, so guard it.
    if entry and entry.javaObject then
        pcall(function() entry.javaObject:setCentreVertically(true) end)
    end
end

local function setupTextEntryBox(entry)
    -- Use the proper setter so the Java text box receives the font immediately.
    if entry.setFont then
        entry:setFont(UIFont.Small)
    else
        entry.font = UIFont.Small
    end

    -- Apply vertical centering now if the Java object already exists, and also after lazy instantiation.
    local originalInstantiate = entry.instantiate
    entry.instantiate = function(textEntry)
        originalInstantiate(textEntry)
        applyTextEntryVerticalCenter(textEntry)
    end
    applyTextEntryVerticalCenter(entry)
end

function MS_PresetsPanel.calculateRequiredSize()
    local padding = math.floor(FONT_HGT_SMALL * 0.35)
    local buttonHeight = math.floor(FONT_HGT_SMALL * 1.45)
    local loadWidth = measureButtonWidth(tr("IGUI_ModernStatus_Load", "Load"), 72, padding)
    local overwriteWidth = measureButtonWidth(tr("IGUI_ModernStatus_Overwrite", "Overwrite"), 96, padding)
    local deleteWidth = buttonHeight
    local actionsWidth = loadWidth + overwriteWidth + deleteWidth + padding * 3
    local nameWidth = math.max(350, getTextManager():MeasureStringX(UIFont.Small, tr("IGUI_ModernStatus_BundledPresets", "Bundled Presets")) * 2)
    local scrollWidth = math.floor(FONT_HGT_SMALL * 0.8)
    local width = padding + nameWidth + actionsWidth + scrollWidth + padding * 2

    -- Keep the preset-name column about 25% wider than the first PresetsPanel layout.
    width = math.max(720, width)
    width = math.min(width, math.max(620, getCore():getScreenWidth() - 40))

    local height = math.min(560, math.max(360, getCore():getScreenHeight() - 80))
    return { width = width, height = height }
end

function MS_PresetsPanel:new(x, y, width, height, playerIndex)
    local size = MS_PresetsPanel.calculateRequiredSize()
    width = width or size.width
    height = height or size.height

    local o = ISPanel:new(x, y, width, height)
    setmetatable(o, self)
    self.__index = self

    o.playerIndex = playerIndex or 0
    o.padding = math.floor(FONT_HGT_SMALL * 0.35)
    o.titleBarHeight = math.floor(FONT_HGT_SMALL * 1.35)
    o.buttonHeight = math.floor(FONT_HGT_SMALL * 1.45)
    o.rowHeight = math.floor(o.buttonHeight * 1.28)
    o.sectionHeight = math.max(FONT_HGT_SMALL + 2, math.floor(o.rowHeight * 0.72))
    o.listGap = math.max(2, math.floor(o.padding * 0.5))
    o.actionRightGap = math.max(2, math.floor(o.padding * 0.5))
    o.sectionArrowSize = math.max(FONT_HGT_SMALL, math.floor(o.buttonHeight * 0.78))
    o.sectionArrowGap = math.max(2, math.floor(o.padding * 0.45))
    o.presetNameIndent = math.max(4, math.floor(o.padding * 0.9))
    o.bundledExpanded = true
    o.savedExpanded = true
    o.keepOpenOnAction = false
    o.filterText = ""
    o.rowControls = {}
    o.pendingRows = nil
    o.pendingRowIndex = 1
    o.pendingRowY = 0
    o.pendingMetrics = nil
    o.pendingListBuild = false
    o.deferInitialRefresh = false
    o.moveWithMouse = false
    o.moving = false

    o.searchFieldBackground = {
        L = getTexture("media/ui/ModernStatus/SettingPanel/LongBackground_L.png"),
        M = getTexture("media/ui/ModernStatus/SettingPanel/LongBackground_M.png"),
        R = getTexture("media/ui/ModernStatus/SettingPanel/LongBackground_R.png")
    }
    o.searchFieldBorder = {
        L = getTexture("media/ui/ModernStatus/SettingPanel/LongBorder_L.png"),
        M = getTexture("media/ui/ModernStatus/SettingPanel/LongBorder_M.png"),
        R = getTexture("media/ui/ModernStatus/SettingPanel/LongBorder_R.png")
    }
    o.clearIcon = getTexture("media/ui/ModernStatus/SettingPanel/Icon_Close.png")
    o.trashIcon = getTexture("media/ui/ModernStatus/ICON/Icon_TrashBin.png")
    o.downArrowIcon = getTexture("media/ui/ModernStatus/SettingPanel/Icon_DownArrow.png")
    o.rightArrowIcon = getTexture("media/ui/ModernStatus/SettingPanel/Icon_RightArrow.png")
    o.lockIcon = getTexture("media/ui/ModernStatus/ICON/Icon_Lock.png")
    o.unlockIcon = getTexture("media/ui/ModernStatus/ICON/Icon_UnLock.png")
    o.titlebarbkg = NinePatchTexture.getSharedTexture("media/ui/ModernStatus/SettingPanel/TitleBarBG.png")
    o.contentBackground = NinePatchTexture.getSharedTexture("media/ui/ModernStatus/SettingPanel/ContentBackground.png")

    return o
end

function MS_PresetsPanel.openForPlayer(playerIndex, anchorX, anchorY)
    if MS_PresetsPanel.CurrentPanel then
        MS_PresetsPanel.CurrentPanel:close()
    end

    local screenWidth = getCore():getScreenWidth()
    local screenHeight = getCore():getScreenHeight()
    local panel = MS_PresetsPanel:new(anchorX or 100, anchorY or 100, nil, nil, playerIndex)
    panel:initialise()
    panel:addToUIManager()
    panel:setVisible(true)

    local x = anchorX or math.floor((screenWidth - panel:getWidth()) / 2)
    local y = anchorY or math.floor((screenHeight - panel:getHeight()) / 2)
    panel:setX(math.max(10, math.min(screenWidth - panel:getWidth() - 10, x)))
    panel:setY(math.max(10, math.min(screenHeight - panel:getHeight() - 10, y)))

    MS_PresetsPanel.CurrentPanel = panel
    return panel
end

function MS_PresetsPanel:initialise()
    ISPanel.initialise(self)
end

function MS_PresetsPanel:createChildren()
    self:createTitleBar()
    self:createFilterBar()
    self:createBottomSaveArea()
    self:createScrollView()

    -- Show the panel immediately, then load preset names and build rows from update() in small chunks.
    -- This avoids doing all file scanning and UI child creation during the opening click.
    self.deferInitialRefresh = true
    self:addLoadingRow()
    self:updateSaveButtonState()
end

function MS_PresetsPanel:createTitleBar()
    local closeSize = FONT_HGT_SMALL
    self.titleLabel = ISLabel:new(self.padding, (self.titleBarHeight - FONT_HGT_SMALL) / 2, self.titleBarHeight, tr("IGUI_ModernStatus_Presets", "Presets"), 1, 1, 1, 1, UIFont.Small, true)
    self.titleLabel:initialise()
    self:addChild(self.titleLabel)

    self.closeButton = MS_SquareButton:new(self.width - closeSize - self.padding, (self.titleBarHeight - closeSize) / 2, closeSize, getTexture("media/ui/ModernStatus/SettingPanel/Icon_Close.png"), self, MS_PresetsPanel.close)
    self.closeButton:setActive(true)
    self.closeButton:setActiveColor(0.8, 0.2, 0.2)
    self.closeButton:initialise()
    self:addChild(self.closeButton)

    -- Keep this button next to the close button, matching the inventory/loot panel lock affordance.
    -- The locked state keeps the Presets panel open after Load, Overwrite and Save actions.
    self.lockButton = MS_SquareButton:new(self.closeButton:getX() - self.padding - closeSize, (self.titleBarHeight - closeSize) / 2, closeSize, self.unlockIcon, self, MS_PresetsPanel.onToggleActionLock)
    self.lockButton:initialise()
    self.lockButton:setIconSizeRatio(0.9)
    self:addChild(self.lockButton)
    self:updateLockButtonIcon()
end

function MS_PresetsPanel:createFilterBar()
    local y = self.titleBarHeight + self.padding
    local height = self.buttonHeight
    local width = self.width - self.padding * 2

    self.filterEntry = ISTextEntryBox:new("", self.padding, y, width, height)
    self.filterEntry:initialise()
    setupTextEntryBox(self.filterEntry)
    self.filterEntry.backgroundColor.a = 0
    self.filterEntry.borderColor.a = 0
    if self.filterEntry.setPlaceholderText then
        self.filterEntry:setPlaceholderText(tr("IGUI_ModernStatus_FilterPresets", "Filter presets..."))
    end
    self.filterEntry.onTextChange = function()
        self:onFilterTextChanged()
    end
    self.filterEntry.prerender = function(searchBox)
        NeatTool.ThreePatch.drawHorizontal(searchBox, -self.padding / 2, 0, searchBox.width + self.padding, searchBox.height, self.searchFieldBackground.L, self.searchFieldBackground.M, self.searchFieldBackground.R, 0.8, 0.1, 0.1, 0.1)
        NeatTool.ThreePatch.drawHorizontal(searchBox, -self.padding / 2, 0, searchBox.width + self.padding, searchBox.height, self.searchFieldBorder.L, self.searchFieldBorder.M, self.searchFieldBorder.R, 1, 0.4, 0.4, 0.4)
    end
    self:addChild(self.filterEntry)

    self.filterClearButton = ISButton:new(self.padding + width - height, y, height, height, "", self, MS_PresetsPanel.onClearFilter)
    self.filterClearButton:initialise()
    self.filterClearButton.borderColor.a = 0
    self.filterClearButton.backgroundColor.a = 0
    self.filterClearButton.backgroundColorMouseOver.a = 0
    self.filterClearButton:setVisible(false)
    self.filterClearButton.prerender = function(button)
        if button:isVisible() and self.clearIcon then
            local iconSize = math.floor(button.height * 0.55)
            local iconX = (button.width - iconSize) / 2
            local iconY = (button.height - iconSize) / 2
            button:drawTextureScaled(self.clearIcon, iconX, iconY, iconSize, iconSize, button:isMouseOver() and 1 or 0.7, 0.8, 0.8, 0.8)
        end
    end
    self:addChild(self.filterClearButton)
end

function MS_PresetsPanel:createBottomSaveArea()
    local y = self.height - self.padding - self.buttonHeight
    local saveText = tr("IGUI_ModernStatus_Save", "Save")
    local saveWidth = measureButtonWidth(saveText, 86, self.padding)
    local entryWidth = self.width - self.padding * 3 - saveWidth

    self.nameEntry = ISTextEntryBox:new("", self.padding, y, entryWidth, self.buttonHeight)
    self.nameEntry:initialise()
    setupTextEntryBox(self.nameEntry)
    self.nameEntry.backgroundColor.a = 0
    self.nameEntry.borderColor.a = 0
    if self.nameEntry.setPlaceholderText then
        self.nameEntry:setPlaceholderText(tr("IGUI_ModernStatus_EnterPresetName", "Enter preset name:"))
    end
    self.nameEntry.onTextChange = function()
        self:updateSaveButtonState()
    end
    self.nameEntry.prerender = function(entry)
        NeatTool.ThreePatch.drawHorizontal(entry, -self.padding / 2, 0, entry.width + self.padding, entry.height, self.searchFieldBackground.L, self.searchFieldBackground.M, self.searchFieldBackground.R, 0.8, 0.1, 0.1, 0.1)
        NeatTool.ThreePatch.drawHorizontal(entry, -self.padding / 2, 0, entry.width + self.padding, entry.height, self.searchFieldBorder.L, self.searchFieldBorder.M, self.searchFieldBorder.R, 1, 0.4, 0.4, 0.4)
    end
    self:addChild(self.nameEntry)

    self.saveButton = MS_LongButton:new(self.padding + entryWidth + self.padding, y, saveWidth, self.buttonHeight, saveText, self, MS_PresetsPanel.onSaveAsPreset)
    self.saveButton:initialise()
    self:addChild(self.saveButton)
end

function MS_PresetsPanel:createScrollView()
    local top = self.titleBarHeight + self.padding + self.buttonHeight + self.listGap
    local bottomSaveTop = self.height - self.padding - self.buttonHeight
    local height = math.max(self.rowHeight * 3, bottomSaveTop - top - self.padding)

    self.scrollView = NIScrollView:new(0, top, self.width, height)
    self.scrollView:initialise()
    self.scrollView:setScrollDirection("vertical")
    self.scrollView:setScrollSensitivity(self.rowHeight)
    self:addChild(self.scrollView)
end

function MS_PresetsPanel:refreshPresetNameCache()
    -- Cache names for the lifetime of the panel so typing in the filter does not reread preset files.
    self.bundledPresetNames = MS_PresetManager.getBundledPresetNames and MS_PresetManager.getBundledPresetNames() or MS_PresetManager.getSortedPresetNames(MS_PresetManager.getBundledPresets())
    self.savedPresetNames = MS_PresetManager.getUserPresetNames and MS_PresetManager.getUserPresetNames() or MS_PresetManager.getSortedPresetNames(MS_PresetManager.getUserPresets())
end

function MS_PresetsPanel:getActionMetrics()
    local loadWidth = measureButtonWidth(tr("IGUI_ModernStatus_Load", "Load"), 72, self.padding)
    local overwriteWidth = measureButtonWidth(tr("IGUI_ModernStatus_Overwrite", "Overwrite"), 96, self.padding)
    local deleteWidth = self.buttonHeight
    local scrollReserve = self.scrollView and self.scrollView.vscroll and self.scrollView.vscroll.width or math.floor(FONT_HGT_SMALL * 0.8)
    local deleteX = self.width - scrollReserve - self.actionRightGap - deleteWidth
    local loadX = deleteX - self.padding - loadWidth
    local overwriteX = loadX - self.padding - overwriteWidth
    local sectionLabelX = self.padding + self.sectionArrowSize + self.sectionArrowGap
    local presetNameX = sectionLabelX + self.presetNameIndent
    local nameWidth = overwriteX - presetNameX - self.padding

    return {
        loadX = loadX,
        overwriteX = overwriteX,
        deleteX = deleteX,
        loadWidth = loadWidth,
        overwriteWidth = overwriteWidth,
        deleteWidth = deleteWidth,
        sectionLabelX = sectionLabelX,
        presetNameX = presetNameX,
        nameWidth = nameWidth
    }
end

function MS_PresetsPanel:getPresetRows()
    local rows = {}
    local filterText = string.lower(trim(self.filterText or ""))

    local function matches(rawName, displayName)
        if filterText == "" then
            return true
        end
        return string.find(string.lower(rawName or ""), filterText, 1, true) ~= nil or
               string.find(string.lower(displayName or ""), filterText, 1, true) ~= nil
    end

    local bundledNames = self.bundledPresetNames or {}
    local bundledRows = {}
    for _, presetName in ipairs(bundledNames) do
        local displayName = MS_PresetManager.getBundledPresetDisplayName(presetName)
        if matches(presetName, displayName) then
            bundledRows[#bundledRows + 1] = { type = "bundled", name = presetName, displayName = displayName }
        end
    end

    local savedNames = self.savedPresetNames or {}
    local savedRows = {}
    for _, presetName in ipairs(savedNames) do
        if matches(presetName, presetName) then
            savedRows[#savedRows + 1] = { type = "saved", name = presetName, displayName = presetName }
        end
    end

    rows[#rows + 1] = { type = "section", sectionKey = "bundled", expanded = self:isSectionExpanded("bundled"), title = tr("IGUI_ModernStatus_BundledPresets", "Bundled Presets") }
    if self:isSectionExpanded("bundled") then
        if #bundledRows == 0 then
            rows[#rows + 1] = { type = "empty", title = filterText == "" and tr("IGUI_ModernStatus_NoBundledPresets", "No bundled presets") or tr("IGUI_ModernStatus_NoMatches", "No matches") }
        else
            for _, row in ipairs(bundledRows) do
                rows[#rows + 1] = row
            end
        end
    end

    rows[#rows + 1] = { type = "section", sectionKey = "saved", expanded = self:isSectionExpanded("saved"), title = tr("IGUI_ModernStatus_SavedPresets", "Saved Presets") }
    if self:isSectionExpanded("saved") then
        if #savedRows == 0 then
            rows[#rows + 1] = { type = "empty", title = filterText == "" and tr("IGUI_ModernStatus_NoSavedPresets", "No saved presets") or tr("IGUI_ModernStatus_NoMatches", "No matches") }
        else
            for _, row in ipairs(savedRows) do
                rows[#rows + 1] = row
            end
        end
    end

    return rows
end

function MS_PresetsPanel:clearRows()
    if not self.scrollView or not self.scrollView.scrollChildren then return end

    for i = #self.scrollView.scrollChildren, 1, -1 do
        self.scrollView:removeScrollChild(self.scrollView.scrollChildren[i])
    end

    self.rowControls = {}
    self.pendingRows = nil
    self.pendingRowIndex = 1
    self.pendingRowY = 0
    self.pendingMetrics = nil
    self.pendingListBuild = false
end

function MS_PresetsPanel:rebuildPresetList()
    self:clearRows()
    if self.scrollView then
        self.scrollView:resetScroll()
    end

    -- Build row widgets incrementally. Creating many buttons at once can cause a visible hitch.
    self.pendingMetrics = self:getActionMetrics()
    self.pendingRows = self:getPresetRows()
    self.pendingRowIndex = 1
    self.pendingRowY = self.listGap
    self.pendingListBuild = true

    if self.scrollView then
        self.scrollView:setScrollHeight(self.scrollView:getHeight())
    end
end

function MS_PresetsPanel:addLoadingRow()
    self:clearRows()
    local metrics = self:getActionMetrics()
    self:addEmptyRow(tr("IGUI_ModernStatus_LoadingPresets", "Loading presets..."), self.listGap, metrics)
    if self.scrollView then
        self.scrollView:setScrollHeight(self.scrollView:getHeight())
    end
end

function MS_PresetsPanel:buildPresetListChunk(maxRows)
    if not self.pendingListBuild or not self.pendingRows then
        return
    end

    local rows = self.pendingRows
    local metrics = self.pendingMetrics or self:getActionMetrics()
    local count = 0

    while self.pendingRowIndex <= #rows and count < (maxRows or 12) do
        local row = rows[self.pendingRowIndex]
        if row.type == "section" then
            self:addSectionRow(row.title, self.pendingRowY, metrics, row.sectionKey, row.expanded)
            self.pendingRowY = self.pendingRowY + self.sectionHeight
        elseif row.type == "empty" then
            self:addEmptyRow(row.title, self.pendingRowY, metrics)
            self.pendingRowY = self.pendingRowY + self.rowHeight
        else
            self:addPresetRow(row, self.pendingRowY, metrics)
            self.pendingRowY = self.pendingRowY + self.rowHeight
        end

        self.pendingRowIndex = self.pendingRowIndex + 1
        count = count + 1
    end

    if self.scrollView then
        self.scrollView:setScrollHeight(math.max(self.pendingRowY + self.padding, self.scrollView:getHeight()))
    end

    if self.pendingRowIndex > #rows then
        self.pendingListBuild = false
        self.pendingRows = nil
        self.pendingMetrics = nil
    end
end

function MS_PresetsPanel:addSectionRow(title, y, metrics, sectionKey, expanded)
    local iconSize = self.sectionArrowSize
    local iconY = y + math.floor((self.sectionHeight - iconSize) / 2)

    -- Draw a lightweight arrow button without a square background, like collapsible inventory stacks.
    local arrowButton = ISButton:new(self.padding, iconY, iconSize, iconSize, "", self, MS_PresetsPanel.onToggleSection)
    arrowButton.presetSectionKey = sectionKey
    arrowButton:initialise()
    arrowButton.borderColor.a = 0
    arrowButton.backgroundColor.a = 0
    arrowButton.backgroundColorMouseOver.a = 0
    if arrowButton.setDisplayBackground then
        arrowButton:setDisplayBackground(false)
    end
    arrowButton.prerender = function(button)
        local isExpanded = self:isSectionExpanded(button.presetSectionKey)
        local icon = isExpanded and self.downArrowIcon or self.rightArrowIcon
        if icon then
            local alpha = button:isMouseOver() and 1 or 0.85
            button:drawTextureScaled(icon, 0, 0, button.width, button.height, alpha, 0.9, 0.9, 0.9)
        end
    end
    self.scrollView:addScrollChild(arrowButton)

    local label = ISLabel:new(metrics.sectionLabelX, y + (self.sectionHeight - FONT_HGT_SMALL) / 2, self.sectionHeight, title, 0.95, 0.7, 0.25, 1, UIFont.Small, true)
    label:initialise()
    self.scrollView:addScrollChild(label)
end

function MS_PresetsPanel:addEmptyRow(title, y, metrics)
    local label = ISLabel:new(metrics.presetNameX, y + (self.rowHeight - FONT_HGT_SMALL) / 2, self.rowHeight, title, 0.65, 0.65, 0.65, 1, UIFont.Small, true)
    label:initialise()
    self.scrollView:addScrollChild(label)
end

function MS_PresetsPanel:addPresetRow(row, y, metrics)
    local nameButton = ISButton:new(metrics.presetNameX, y, metrics.nameWidth, self.rowHeight, "", self, nil)
    nameButton:initialise()
    nameButton.displayBackground = false
    nameButton.displayName = row.displayName
    nameButton.prerender = function(button)
        if button:isMouseOver() then
            button:drawRect(0, 0, button:getWidth(), button:getHeight(), 0.22, 0.5, 0.5, 0.5)
        end
        local textY = (self.rowHeight - FONT_HGT_SMALL) / 2
        button:drawText(button.displayName or "", 0, textY, 1, 1, 1, 1, UIFont.Small)
    end
    self.scrollView:addScrollChild(nameButton)

    local buttonY = y + (self.rowHeight - self.buttonHeight) / 2
    local overwriteButton = nil
    local deleteButton = nil

    if row.type == "saved" then
        overwriteButton = MS_LongButton:new(metrics.overwriteX, buttonY, metrics.overwriteWidth, self.buttonHeight, tr("IGUI_ModernStatus_Overwrite", "Overwrite"), self, MS_PresetsPanel.onOverwritePreset)
        overwriteButton.presetName = row.name
        overwriteButton:initialise()
        self.scrollView:addScrollChild(overwriteButton)
    end

    local loadButton = MS_LongButton:new(metrics.loadX, buttonY, metrics.loadWidth, self.buttonHeight, tr("IGUI_ModernStatus_Load", "Load"), self, MS_PresetsPanel.onLoadPreset)
    loadButton.presetType = row.type
    loadButton.presetName = row.name
    loadButton:initialise()
    self.scrollView:addScrollChild(loadButton)

    if row.type == "saved" then
        deleteButton = MS_SquareButton:new(metrics.deleteX, buttonY, metrics.deleteWidth, self.trashIcon, self, MS_PresetsPanel.onDeletePreset)
        deleteButton.presetName = row.name
        deleteButton:initialise()
        deleteButton:setIconSizeRatio(0.72)
        if deleteButton.setTooltip then
            deleteButton:setTooltip(tr("IGUI_ModernStatus_DeletePreset", "Delete Preset"))
        end
        self.scrollView:addScrollChild(deleteButton)
    end

    self.rowControls[#self.rowControls + 1] = {
        nameButton = nameButton,
        loadButton = loadButton,
        overwriteButton = overwriteButton,
        deleteButton = deleteButton
    }
end

function MS_PresetsPanel:onFilterTextChanged()
    self.filterText = self.filterEntry and self.filterEntry:getText() or ""
    if self.filterClearButton then
        self.filterClearButton:setVisible(trim(self.filterText) ~= "")
    end
    self:rebuildPresetList()
end

function MS_PresetsPanel:onClearFilter()
    if self.filterEntry then
        self.filterEntry:setText("")
    end
    self.filterText = ""
    if self.filterClearButton then
        self.filterClearButton:setVisible(false)
    end
    self:rebuildPresetList()
end

function MS_PresetsPanel:isSectionExpanded(sectionKey)
    if sectionKey == "bundled" then
        return self.bundledExpanded ~= false
    end
    if sectionKey == "saved" then
        return self.savedExpanded ~= false
    end
    return true
end

function MS_PresetsPanel:setSectionExpanded(sectionKey, expanded)
    if sectionKey == "bundled" then
        self.bundledExpanded = expanded and true or false
    elseif sectionKey == "saved" then
        self.savedExpanded = expanded and true or false
    end
end

function MS_PresetsPanel:onToggleSection(button)
    if not button or not button.presetSectionKey then return end
    self:setSectionExpanded(button.presetSectionKey, not self:isSectionExpanded(button.presetSectionKey))
    self:rebuildPresetList()
end

function MS_PresetsPanel:updateLockButtonIcon()
    if self.lockButton then
        self.lockButton:setIcon(self.keepOpenOnAction and self.lockIcon or self.unlockIcon)
    end
end

function MS_PresetsPanel:onToggleActionLock()
    self.keepOpenOnAction = not self.keepOpenOnAction
    self:updateLockButtonIcon()
end

function MS_PresetsPanel:shouldCloseAfterAction()
    -- Icon_Lock means the panel is locked open after Load, Overwrite and Save actions.
    return not self.keepOpenOnAction
end

function MS_PresetsPanel:updateSaveButtonState()
    if not self.saveButton or not self.nameEntry then return end
    -- Keep the save button disabled only for blank names; invalid long/control-character names show a popup on click.
    self.saveButton:setEnable(trim(self.nameEntry:getText()) ~= "")
end

function MS_PresetsPanel:showMessage(text)
    local modal = ISModalDialog:new(0, 0, 360, 150, text, false, nil, nil, self.playerIndex)
    modal:initialise()
    modal:addToUIManager()
end

function MS_PresetsPanel:saveCurrentAs(name)
    local ok = MS_PresetManager.saveCurrentPreset(self.playerIndex, name)
    if ok then
        if self:shouldCloseAfterAction() then
            self:close()
        else
            -- Refresh saved preset names when the panel is locked open after Save/Overwrite.
            self:refreshPresetNameCache()
            self:rebuildPresetList()
        end
    end
end

function MS_PresetsPanel:onSaveAsPreset(button)
    local valid, presetName = isValidPresetName(self.nameEntry and self.nameEntry:getText() or "")
    if not valid then
        self:showMessage(tr("IGUI_ModernStatus_InvalidPresetName", "Preset name cannot be blank, too long, or contain line breaks/control characters."))
        self:updateSaveButtonState()
        return
    end

    self:saveCurrentAs(presetName)
end

function MS_PresetsPanel:onOverwritePreset(button)
    if not button or not button.presetName then return end
    self:saveCurrentAs(button.presetName)
end

function MS_PresetsPanel:onDeletePreset(button)
    if not button or not button.presetName then return end
    if MS_PresetManager.deleteUserPreset(button.presetName) then
        self:refreshPresetNameCache()
        self:rebuildPresetList()
    end
end

function MS_PresetsPanel:onLoadPreset(button)
    if not button or not button.presetName then return end

    local preset = nil
    if button.presetType == "bundled" then
        if MS_PresetManager.getBundledPresetByName then
            preset = MS_PresetManager.getBundledPresetByName(button.presetName)
        else
            preset = MS_PresetManager.getBundledPresets()[button.presetName]
        end
    else
        if MS_PresetManager.getUserPresetByName then
            preset = MS_PresetManager.getUserPresetByName(button.presetName)
        else
            preset = MS_PresetManager.getUserPresets()[button.presetName]
        end
    end

    if MS_PresetManager.applyPreset(self.playerIndex, preset) then
        if self:shouldCloseAfterAction() then
            self:close()
        end
    end
end

function MS_PresetsPanel:update()
    ISPanel.update(self)

    if self.deferInitialRefresh then
        self.deferInitialRefresh = false
        self:refreshPresetNameCache()
        self:rebuildPresetList()
    end

    if self.pendingListBuild then
        self:buildPresetListChunk(10)
    end
end

function MS_PresetsPanel:prerender()
    ISPanel.prerender(self)
    if self.titlebarbkg then
        self.titlebarbkg:render(self:getAbsoluteX(), self:getAbsoluteY(), self:getWidth(), self.titleBarHeight, 0.5, 0.5, 0.5, 0.95)
    end
    if self.contentBackground then
        self.contentBackground:render(self:getAbsoluteX(), self:getAbsoluteY() + self.titleBarHeight, self:getWidth(), self:getHeight() - self.titleBarHeight, 0.1, 0.1, 0.1, 0.8)
    end
end

function MS_PresetsPanel:onMouseDown(x, y)
    self.downX = x
    self.downY = y
    self.moving = y <= self.titleBarHeight
    if self.moving then
        self:bringToTop()
    end
end

function MS_PresetsPanel:onMouseMove(dx, dy)
    self.mouseOver = true
    if self.moving then
        self:setX(self.x + dx)
        self:setY(self.y + dy)
        self:bringToTop()
    end
end

function MS_PresetsPanel:onMouseMoveOutside(dx, dy)
    self.mouseOver = false
    if self.moving then
        self:setX(self.x + dx)
        self:setY(self.y + dy)
        self:bringToTop()
    end
end

function MS_PresetsPanel:onMouseUp(x, y)
    self.moving = false
end

function MS_PresetsPanel:onMouseUpOutside(x, y)
    self.moving = false
end

function MS_PresetsPanel:close()
    self:setVisible(false)
    self:removeFromUIManager()
    if MS_PresetsPanel.CurrentPanel == self then
        MS_PresetsPanel.CurrentPanel = nil
    end
end

return MS_PresetsPanel
