local addonName, addonTable = ...
---@type MKA
local MKA = addonTable.MKA
local core = addonTable
local TRANSLATE = core.TRANSLATE or function(msg) return msg end

core.OptionsTab = core.OptionsTab or {}
local OptionsTab = core.OptionsTab

local OPTIONS_CONTENT_HEIGHT = 452
local OPTIONS_FORM_WIDTH = 820
local OPTIONS_CONTROL_COLUMN_X = 270
local OPTIONS_CONTROL_INSET_X = 14
local OPTIONS_ROW_HEIGHT = 28
local OPTIONS_TITLE_TO_CONTROLS_GAP = 14
local OPTIONS_SECTION_GAP = 16
local OPTIONS_CONTENT_TOP_OFFSET = 20
local OPTIONS_CONTENT_BOTTOM_PADDING = 20
local OPTIONS_LABEL_TEXT_SIZE = 14
local OPTIONS_CHECK_BUTTON_SIZE = 26
local OPTIONS_DROPDOWN_WIDTH = 140
local OPTIONS_DROPDOWN_HEIGHT = 28
local OPTIONS_TEXT_INPUT_HEIGHT = 28
local OPTIONS_TEXT_INPUT_MAX_LETTERS = 140
local OPTIONS_TACTICAL_MESSAGE_MAX_BYTES = core.DUNGEON_TELEPORT_CHAT_MESSAGE_MAX_BYTES or 255
local OPTIONS_CONFIRM_BUTTON_WIDTH = 64
local OPTIONS_CONFIRM_BUTTON_HEIGHT = 28
local OPTIONS_CONFIRM_BUTTON_GAP = 8
local OPTIONS_CLEAR_BUTTON_WIDTH = 64
local OPTIONS_DEFAULT_BUTTON_WIDTH = 86
local OPTIONS_DEFAULT_BUTTON_HEIGHT = 28
local OPTIONS_PREVIEW_BUTTON_WIDTH = 64
local OPTIONS_PREVIEW_BUTTON_HEIGHT = 28
local OPTIONS_SCROLLBAR_RIGHT_OFFSET = 18
local OPTIONS_MESSAGE_INPUT_WIDTH = OPTIONS_FORM_WIDTH - OPTIONS_CONTROL_COLUMN_X - OPTIONS_CONFIRM_BUTTON_WIDTH - OPTIONS_CONFIRM_BUTTON_GAP

core.MKA_OPTIONS_LAYOUT = core.MKA_OPTIONS_LAYOUT or {
    panelRowCount = 8,
    panelRowHeight = 40,
    panelSectionGap = 8,
    panelLabelInsetX = 28,
    panelControlInsetX = 34,
    previewPanelHeight = 78,
}
core.MKA_OPTIONS_LAYOUT.settingsPanelHeight = core.MKA_OPTIONS_LAYOUT.panelRowCount * core.MKA_OPTIONS_LAYOUT.panelRowHeight

core.MKA_OPTIONS_PREVIEW = core.MKA_OPTIONS_PREVIEW or {
    topGap = 8,
    height = 34,
    iconSize = 22,
    iconInsetX = 10,
    textInsetLeft = 38,
    textInsetRight = 10,
    iconTexture = "Interface\\AddOns\\" .. addonName .. "\\Art\\Ui\\SayIcon.png",
}

local STARTUP_TAB_OPTIONS = {
    { value = "characters", text = "TAB.CHARACTERS" },
    { value = "group", text = "TAB.GROUP" },
    { value = "carpool", text = "TAB.CARPOOL" },
    { value = "dungeons", text = "TAB.DUNGEONS" },
    { value = "meetingstone", text = "TAB.MEETINGSTONE" },
    { value = "findgroup", text = "TAB.FIND_GROUP" },
    { value = "options", text = "TAB.OPTIONS" },
}

local AUTO_OPEN_COMPLETED_RUN_OPTIONS = {
    { value = "off", text = "OPTIONS.AUTO_OPEN_OFF" },
    { value = "characters", text = "TAB.CHARACTERS" },
    { value = "group", text = "TAB.GROUP" },
    { value = "carpool", text = "TAB.CARPOOL" },
    { value = "dungeons", text = "TAB.DUNGEONS" },
    { value = "meetingstone", text = "TAB.MEETINGSTONE" },
    { value = "findgroup", text = "TAB.FIND_GROUP" },
}

local function NormalizeOptionsTextInputValue(value, maxBytes)
    value = tostring(value or "")
    if maxBytes and core.TruncateUtf8ToByteLimit then
        return core.TruncateUtf8ToByteLimit(value, maxBytes)
    end
    return value
end

local function SkinOptionsNativeCheckButton(button, checkSize)
    if not button then
        return
    end

    checkSize = checkSize or OPTIONS_CHECK_BUTTON_SIZE
    local textures = {
        button:GetNormalTexture(),
        button:GetPushedTexture(),
        button:GetHighlightTexture(),
        button:GetCheckedTexture(),
        button:GetDisabledCheckedTexture(),
    }

    for _, texture in ipairs(textures) do
        if texture then
            texture:ClearAllPoints()
            texture:SetPoint("CENTER", button, "CENTER", 0, 0)
            texture:SetSize(checkSize, checkSize)
        end
    end
end

local function CreateOptionsNativeCheckButton(parent, onClick)
    local button = CreateFrame("CheckButton", nil, parent, "UICheckButtonTemplate")
    button:SetSize(OPTIONS_CHECK_BUTTON_SIZE, OPTIONS_CHECK_BUTTON_SIZE)
    button:EnableMouse(true)
    SkinOptionsNativeCheckButton(button, OPTIONS_CHECK_BUTTON_SIZE)
    button:SetScript("OnClick", function(self)
        if onClick then
            onClick(self:GetChecked() == true)
        end
    end)
    return button
end

local function CreateOptionsTextInput(parent, owner, getValue, setValue, width, options)
    local meetingStoneForm = core.MeetingStoneForm or {}
    local holder = CreateFrame("Frame", nil, parent)
    options = options or {}
    width = width or OPTIONS_MESSAGE_INPUT_WIDTH
    holder:SetSize(width, OPTIONS_TEXT_INPUT_HEIGHT)
    holder.mkaMaxBytes = tonumber(options.maxBytes)

    if meetingStoneForm.AddTitleHolderBorder then
        meetingStoneForm.AddTitleHolderBorder(holder)
    end

    local editBox
    if meetingStoneForm.CreateInput then
        editBox = meetingStoneForm.CreateInput(holder, width, OPTIONS_TEXT_INPUT_HEIGHT, false)
        editBox:SetAllPoints(holder)
        if editBox.mkaMeetingStoneInputBorder then
            editBox.mkaMeetingStoneInputBorder:Hide()
        end
    else
        editBox = CreateFrame("EditBox", nil, holder, "InputBoxTemplate")
        editBox:SetAllPoints(holder)
    end

    editBox:SetAutoFocus(false)
    editBox:SetMaxLetters(tonumber(options.maxLetters) or holder.mkaMaxBytes or OPTIONS_TEXT_INPUT_MAX_LETTERS)
    if core.ApplyFont then
        core.ApplyFont(editBox, 14, "")
    end
    editBox:SetTextColor(1, 0.96, 0.86, 1)

    if type(options.placeholder) == "string" and options.placeholder ~= "" then
        local placeholder = editBox:CreateFontString(nil, "OVERLAY", "GameFontDisable")
        placeholder:SetPoint("LEFT", editBox, "LEFT", 12, 0)
        placeholder:SetPoint("RIGHT", editBox, "RIGHT", -12, 0)
        placeholder:SetHeight(OPTIONS_TEXT_INPUT_HEIGHT)
        placeholder:SetJustifyH("LEFT")
        placeholder:SetJustifyV("MIDDLE")
        placeholder:SetText(options.placeholder)
        placeholder:SetTextColor(0.55, 0.55, 0.55, 0.88)
        placeholder:SetWordWrap(false)
        if core.ApplyFont then
            core.ApplyFont(placeholder, 14, "")
        end
        holder.mkaPlaceholder = placeholder
        holder.mkaRefreshPlaceholder = function(self)
            if not self.mkaPlaceholder then
                return
            end
            local value = editBox:GetText() or ""
            if value == "" then
                self.mkaPlaceholder:Show()
            else
                self.mkaPlaceholder:Hide()
            end
        end
    end

    holder.mkaGetCurrentText = function()
        return NormalizeOptionsTextInputValue(editBox:GetText(), holder.mkaMaxBytes)
    end
    holder.mkaGetSavedText = function()
        if owner and getValue then
            return getValue(owner) or ""
        end
        return ""
    end
    holder.mkaRefreshControls = function(self)
        if self.mkaRefreshActionButtons then
            self:mkaRefreshActionButtons()
        end
    end
    holder.mkaCommitText = function(self)
        if setValue and owner then
            setValue(owner, self:mkaGetCurrentText())
        end
        self:mkaRefreshControls()
    end

    editBox:SetScript("OnEnterPressed", function(self)
        holder:mkaCommitText()
        self:ClearFocus()
    end)
    editBox:HookScript("OnTextChanged", function(self)
        local currentText = self:GetText() or ""
        local normalizedText = NormalizeOptionsTextInputValue(currentText, holder.mkaMaxBytes)
        if normalizedText ~= currentText then
            self.mkaSuppressTextChanged = true
            self:SetText(normalizedText)
            if self.SetCursorPosition then
                self:SetCursorPosition(#normalizedText)
            end
            self.mkaSuppressTextChanged = nil
            currentText = normalizedText
        end

        if not self.mkaSuppressTextChanged and holder.mkaOnTextChanged then
            holder:mkaOnTextChanged(currentText)
        end
        if holder.mkaRefreshPlaceholder then
            holder:mkaRefreshPlaceholder()
        end
        holder:mkaRefreshControls()
    end)
    editBox:HookScript("OnEditFocusLost", function()
        holder:mkaCommitText()
    end)
    editBox:SetScript("OnEscapePressed", function(self)
        if owner and getValue then
            self.mkaSuppressTextChanged = true
            self:SetText(NormalizeOptionsTextInputValue(getValue(owner), holder.mkaMaxBytes))
            self.mkaSuppressTextChanged = nil
        end
        holder:mkaRefreshControls()
        self:ClearFocus()
    end)

    holder.mkaEditBox = editBox
    holder.mkaSetDisplayText = function(self, value)
        if editBox.HasFocus and editBox:HasFocus() then
            self:mkaRefreshControls()
            return
        end

        value = NormalizeOptionsTextInputValue(value, holder.mkaMaxBytes)
        if editBox:GetText() == value then
            if self.mkaRefreshPlaceholder then
                self:mkaRefreshPlaceholder()
            end
            self:mkaRefreshControls()
            return
        end

        editBox.mkaSuppressTextChanged = true
        editBox:SetText(value)
        editBox.mkaSuppressTextChanged = nil
        if self.mkaRefreshPlaceholder then
            self:mkaRefreshPlaceholder()
        end
        self:mkaRefreshControls()
    end

    if owner and getValue then
        holder:mkaSetDisplayText(getValue(owner) or "")
    elseif holder.mkaRefreshPlaceholder then
        holder:mkaRefreshPlaceholder()
    end

    return holder
end

local function SetOptionsActionButtonEnabled(button, enabled)
    if not button then
        return
    end

    enabled = enabled == true
    button.mkaOptionsActionEnabled = enabled
    if enabled then
        if button.Enable then
            button:Enable()
        end
        if button.EnableMouse then
            button:EnableMouse(true)
        end
        button:SetAlpha(1)
    else
        if button.Disable then
            button:Disable()
        end
        if button.EnableMouse then
            button:EnableMouse(false)
        end
        button:SetAlpha(0.45)
    end
end

local function CreateOptionsConfirmButton(parent, onClick)
    local meetingStoneForm = core.MeetingStoneForm or {}
    local button
    if meetingStoneForm.CreateSystemButton then
        button = meetingStoneForm.CreateSystemButton(parent, nil, OPTIONS_CONFIRM_BUTTON_WIDTH, OPTIONS_CONFIRM_BUTTON_HEIGHT, TRANSLATE("COMMON.CONFIRM"), 1)
    else
        button = CreateFrame("Button", nil, parent, "UIPanelButtonTemplate")
        button:SetText(TRANSLATE("COMMON.CONFIRM"))
    end

    button:SetSize(OPTIONS_CONFIRM_BUTTON_WIDTH, OPTIONS_CONFIRM_BUTTON_HEIGHT)
    if core.ApplyFont then
        local fontString = button.Text or (button.GetFontString and button:GetFontString())
        core.ApplyFont(fontString, 13, "")
    end
    button:SetScript("OnClick", function()
        if button.mkaOptionsActionEnabled == false then
            return
        end
        if onClick then
            onClick()
        end
    end)
    return button
end

local function CreateOptionsClearButton(parent, onClick)
    local meetingStoneForm = core.MeetingStoneForm or {}
    local button
    if meetingStoneForm.CreateSystemButton then
        button = meetingStoneForm.CreateSystemButton(parent, nil, OPTIONS_CLEAR_BUTTON_WIDTH, OPTIONS_CONFIRM_BUTTON_HEIGHT, TRANSLATE("COMMON.CLEAR"), 1)
    else
        button = CreateFrame("Button", nil, parent, "UIPanelButtonTemplate")
        button:SetText(TRANSLATE("COMMON.CLEAR"))
    end

    button:SetSize(OPTIONS_CLEAR_BUTTON_WIDTH, OPTIONS_CONFIRM_BUTTON_HEIGHT)
    if core.ApplyFont then
        local fontString = button.Text or (button.GetFontString and button:GetFontString())
        core.ApplyFont(fontString, 13, "")
    end
    button:SetScript("OnClick", function()
        if button.mkaOptionsActionEnabled == false then
            return
        end
        if onClick then
            onClick()
        end
    end)
    return button
end

local function CreateOptionsPromptPreviewButton(parent, onClick)
    local meetingStoneForm = core.MeetingStoneForm or {}
    local button
    if meetingStoneForm.CreateSystemButton then
        button = meetingStoneForm.CreateSystemButton(parent, nil, OPTIONS_PREVIEW_BUTTON_WIDTH, OPTIONS_PREVIEW_BUTTON_HEIGHT, TRANSLATE("COMMON.PREVIEW"), 1)
    else
        button = CreateFrame("Button", nil, parent, "UIPanelButtonTemplate")
        button:SetText(TRANSLATE("COMMON.PREVIEW"))
    end

    button:SetSize(OPTIONS_PREVIEW_BUTTON_WIDTH, OPTIONS_PREVIEW_BUTTON_HEIGHT)
    if core.ApplyFont then
        local fontString = button.Text or (button.GetFontString and button:GetFontString())
        core.ApplyFont(fontString, 13, "")
    end
    button:SetScript("OnClick", function()
        if onClick then
            onClick()
        end
    end)
    return button
end

local function CreateOptionsDefaultButton(parent, onClick)
    local meetingStoneForm = core.MeetingStoneForm or {}
    local button
    if meetingStoneForm.CreateSystemButton then
        button = meetingStoneForm.CreateSystemButton(parent, nil, OPTIONS_DEFAULT_BUTTON_WIDTH, OPTIONS_DEFAULT_BUTTON_HEIGHT, TRANSLATE("OPTIONS.DEFAULT_SETTINGS"), 1)
    else
        button = CreateFrame("Button", nil, parent, "UIPanelButtonTemplate")
        button:SetText(TRANSLATE("OPTIONS.DEFAULT_SETTINGS"))
    end

    button:SetSize(OPTIONS_DEFAULT_BUTTON_WIDTH, OPTIONS_DEFAULT_BUTTON_HEIGHT)
    if core.ApplyFont then
        local fontString = button.Text or (button.GetFontString and button:GetFontString())
        core.ApplyFont(fontString, 13, "")
    end
    button:SetScript("OnClick", function()
        if onClick then
            onClick()
        end
    end)
    return button
end

function core.FormatRaidTargetIconsForPreview(text)
    if type(text) ~= "string" or text == "" then
        return text or ""
    end

    return text:gsub("{[Rr][Tt]([1-8])}", function(index)
        return "|TInterface\\TargetingFrame\\UI-RaidTargetingIcon_" .. index .. ":16:16:0:0|t"
    end)
end

function core.ColorizeOptionsPreviewText(text, channel)
    local value = channel or "SAY"
    local info = (CHAT_TYPE_INFO and CHAT_TYPE_INFO[value]) or (ChatTypeInfo and ChatTypeInfo[value])
    local r = info and info.r or 1
    local g = info and info.g or 1
    local b = info and info.b or 1
    return string.format("|cff%02x%02x%02x%s|r", math.floor(r * 255 + 0.5), math.floor(g * 255 + 0.5), math.floor(b * 255 + 0.5), core.FormatRaidTargetIconsForPreview(text) or "")
end

function core.CreateOptionsAnnouncementPreview(parent, owner)
    local style = core.MKA_OPTIONS_PREVIEW
    local frame = CreateFrame("Frame", nil, parent, "BackdropTemplate")
    frame:SetHeight(style.height)
    if frame.SetBackdrop then
        frame:SetBackdrop({
            bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background-Dark",
            edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
            tile = true,
            tileSize = 16,
            edgeSize = 12,
            insets = { left = 3, right = 3, top = 3, bottom = 3 },
        })
        frame:SetBackdropColor(0, 0, 0, 0.45)
        frame:SetBackdropBorderColor(0.55, 0.45, 0.25, 0.9)
    end

    local icon = frame:CreateTexture(nil, "ARTWORK")
    icon:SetSize(style.iconSize, style.iconSize)
    icon:SetPoint("LEFT", frame, "LEFT", style.iconInsetX, 0)
    icon:SetTexture(style.iconTexture)

    local text = frame:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    text:SetPoint("TOPLEFT", frame, "TOPLEFT", style.textInsetLeft, 0)
    text:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -style.textInsetRight, 0)
    text:SetJustifyH("LEFT")
    text:SetJustifyV("MIDDLE")
    text:SetWordWrap(false)
    if core.ApplyFont then
        core.ApplyFont(text, 13, "")
    end
    frame.text = text

    frame.mkaSetPreviewMessage = function(self, messageTemplate)
        local formatter = core.DungeonTeleport and core.DungeonTeleport.FormatPreviewMessage
        local previewText
        if formatter then
            previewText = formatter(owner, messageTemplate)
        else
            previewText = tostring(messageTemplate or "")
        end

        local channel = owner and owner.GetGroupChannel and owner:GetGroupChannel() or "SAY"
        text:SetText(core.ColorizeOptionsPreviewText(previewText, channel))
    end

    return frame
end

function core.ApplyOptionsFeaturePanelStyle(frame)
    if not frame or not frame.SetBackdrop then
        return
    end

    frame:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8X8",
        edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
        tile = false,
        edgeSize = 12,
        insets = { left = 3, right = 3, top = 3, bottom = 3 },
    })
    frame:SetBackdropColor(0.03, 0.02, 0.01, 0.30)
    frame:SetBackdropBorderColor(0.55, 0.43, 0.18, 0.75)
end

function core.CreateOptionsFeaturePanel(parent)
    local frame = CreateFrame("Frame", nil, parent, "BackdropTemplate")
    core.ApplyOptionsFeaturePanelStyle(frame)
    return frame
end

function core.CreateOptionsPanelRule(parent)
    local rule = parent:CreateTexture(nil, "ARTWORK")
    rule:SetTexture("Interface\\Buttons\\WHITE8X8")
    rule:SetVertexColor(0.72, 0.52, 0.22, 0.16)
    return rule
end

local function GetOptionsSettingsPanelHeight(rowCount)
    return math.max(tonumber(rowCount) or 0, 0) * core.MKA_OPTIONS_LAYOUT.panelRowHeight
end

function core.CreateOptionsSettingsPanel(parent, rowCount)
    local layout = core.MKA_OPTIONS_LAYOUT
    local panel = core.CreateOptionsFeaturePanel(parent)
    panel.rows = {}
    rowCount = math.max(tonumber(rowCount) or layout.panelRowCount, 0)

    local columnDivider = core.CreateOptionsPanelRule(panel)
    columnDivider:SetPoint("TOPLEFT", panel, "TOPLEFT", OPTIONS_CONTROL_COLUMN_X, -1)
    columnDivider:SetPoint("BOTTOMLEFT", panel, "BOTTOMLEFT", OPTIONS_CONTROL_COLUMN_X, 1)
    columnDivider:SetWidth(1)
    panel.columnDivider = columnDivider

    for rowIndex = 1, rowCount do
        local row = CreateFrame("Frame", nil, panel)
        row:SetPoint("TOPLEFT", panel, "TOPLEFT", 0, -((rowIndex - 1) * layout.panelRowHeight))
        row:SetPoint("TOPRIGHT", panel, "TOPRIGHT", 0, -((rowIndex - 1) * layout.panelRowHeight))
        row:SetHeight(layout.panelRowHeight)
        panel.rows[rowIndex] = row

        local rowBackground = row:CreateTexture(nil, "BACKGROUND", nil, -1)
        rowBackground:SetAllPoints(row)
        rowBackground:SetTexture("Interface\\Buttons\\WHITE8X8")
        rowBackground:SetVertexColor(1, 0.82, 0, rowIndex % 2 == 1 and 0.018 or 0.008)
        row.background = rowBackground

        if rowIndex > 1 then
            local rule = core.CreateOptionsPanelRule(panel)
            rule:SetPoint("TOPLEFT", row, "TOPLEFT", 16, 0)
            rule:SetPoint("TOPRIGHT", row, "TOPRIGHT", -16, 0)
            rule:SetHeight(1)
            row.topRule = rule
        end
    end

    return panel
end

function core.CreateOptionsRowLabel(row, text)
    local layout = core.MKA_OPTIONS_LAYOUT
    local label = row:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    label:SetPoint("LEFT", row, "LEFT", layout.panelLabelInsetX, 0)
    label:SetSize(OPTIONS_CONTROL_COLUMN_X - layout.panelLabelInsetX - 16, OPTIONS_ROW_HEIGHT)
    label:SetJustifyH("LEFT")
    label:SetJustifyV("MIDDLE")
    label:SetText(text or "")
    if core.ApplyFont then
        core.ApplyFont(label, OPTIONS_LABEL_TEXT_SIZE, "")
    end
    return label
end

function core.AnchorOptionsRowControl(control, row)
    if not control or not row then
        return
    end

    local layout = core.MKA_OPTIONS_LAYOUT
    control:ClearAllPoints()
    control:SetPoint("LEFT", row, "LEFT", OPTIONS_CONTROL_COLUMN_X + layout.panelControlInsetX, 0)
end

function core.AnchorOptionsMessageControls(row, input, confirmButton)
    if not row or not input or not confirmButton then
        return
    end

    local layout = core.MKA_OPTIONS_LAYOUT
    confirmButton:ClearAllPoints()
    confirmButton:SetPoint("RIGHT", row, "RIGHT", -layout.panelControlInsetX, 0)

    input:ClearAllPoints()
    input:SetPoint("LEFT", row, "LEFT", OPTIONS_CONTROL_COLUMN_X + layout.panelControlInsetX, 0)
    input:SetPoint("RIGHT", confirmButton, "LEFT", -OPTIONS_CONFIRM_BUTTON_GAP, 0)
    input:SetHeight(OPTIONS_TEXT_INPUT_HEIGHT)
end

local function AnchorOptionsTacticalMessageControls(row, input, confirmButton, clearButton)
    if not row or not input or not confirmButton or not clearButton then
        return
    end

    local layout = core.MKA_OPTIONS_LAYOUT
    clearButton:ClearAllPoints()
    clearButton:SetPoint("RIGHT", row, "RIGHT", -layout.panelControlInsetX, 0)

    confirmButton:ClearAllPoints()
    confirmButton:SetPoint("RIGHT", clearButton, "LEFT", -OPTIONS_CONFIRM_BUTTON_GAP, 0)

    input:ClearAllPoints()
    input:SetPoint("LEFT", row, "LEFT", OPTIONS_CONTROL_COLUMN_X + layout.panelControlInsetX, 0)
    input:SetPoint("RIGHT", confirmButton, "LEFT", -OPTIONS_CONFIRM_BUTTON_GAP, 0)
    input:SetHeight(OPTIONS_TEXT_INPUT_HEIGHT)
end

local function AnchorOptionsPromptControls(row, toggle, previewButton)
    if not row or not toggle or not previewButton then
        return
    end

    local layout = core.MKA_OPTIONS_LAYOUT
    previewButton:ClearAllPoints()
    previewButton:SetPoint("RIGHT", row, "RIGHT", -layout.panelControlInsetX, 0)

    toggle:ClearAllPoints()
    toggle:SetPoint("LEFT", row, "LEFT", OPTIONS_CONTROL_COLUMN_X + layout.panelControlInsetX, 0)
end

function core.CreateOptionsPreviewPanel(parent)
    local panel = core.CreateOptionsFeaturePanel(parent)

    local title = panel:CreateFontString(nil, "OVERLAY", "GameFontNormal")
    title:SetPoint("TOPLEFT", panel, "TOPLEFT", 16, -6)
    title:SetPoint("TOPRIGHT", panel, "TOPRIGHT", -16, -6)
    title:SetHeight(18)
    title:SetJustifyH("LEFT")
    title:SetJustifyV("MIDDLE")
    title:SetText(TRANSLATE("OPTIONS.PREVIEW_EFFECT"))
    title:SetTextColor(1, 0.82, 0, 1)
    if core.ApplyFont then
        core.ApplyFont(title, 14, "OUTLINE")
    end

    panel.title = title
    return panel
end

local function UpdateOptionsScrollState(widgets)
    if not widgets or not widgets.optionsScrollFrame or not widgets.optionsContent or not widgets.optionsScrollBar then
        return
    end

    local scrollFrame = widgets.optionsScrollFrame
    local content = widgets.optionsContent
    local scrollBar = widgets.optionsScrollBar

    if scrollFrame.UpdateScrollChildRect then
        scrollFrame:UpdateScrollChildRect()
    end

    local viewHeight = scrollFrame:GetHeight() or 0
    content:SetHeight(math.max(content.mkaOptionsContentHeight or OPTIONS_CONTENT_HEIGHT, viewHeight))

    local contentHeight = content:GetHeight() or 0
    local frameScrollRange = scrollFrame.GetVerticalScrollRange and scrollFrame:GetVerticalScrollRange() or 0
    local maxScroll = math.max(frameScrollRange, contentHeight - viewHeight, 0)
    local currentValue
    if scrollBar.mkaGetScrollValue then
        currentValue = scrollBar.mkaGetScrollValue()
    elseif scrollBar.GetValue then
        currentValue = scrollBar:GetValue()
    else
        currentValue = 0
    end
    currentValue = math.min(currentValue or 0, maxScroll)

    if scrollBar.mkaSetScrollRange then
        scrollBar.mkaSetScrollRange(0, maxScroll)
    elseif scrollBar.SetMinMaxValues then
        scrollBar:SetMinMaxValues(0, maxScroll)
    end
    if scrollBar.SetVisibleExtentPercentage then
        local visibleExtentPercentage = 0
        if viewHeight > 0 then
            visibleExtentPercentage = math.min(math.max(viewHeight / (maxScroll + viewHeight), 0), 1)
        end
        scrollBar:SetVisibleExtentPercentage(visibleExtentPercentage)
    end
    if scrollBar.SetPanExtentPercentage then
        local panExtentPercentage = maxScroll > 0 and math.min(math.max(36 / maxScroll, 0), 1) or 0
        scrollBar:SetPanExtentPercentage(panExtentPercentage)
    end
    if scrollBar.SetScrollAllowed then
        scrollBar:SetScrollAllowed(maxScroll > 0)
    end
    if scrollBar.mkaSetScrollValue then
        scrollBar.mkaSetScrollValue(currentValue)
    elseif scrollBar.SetValue then
        scrollBar:SetValue(currentValue)
    end
    scrollFrame:SetVerticalScroll(currentValue)

    if maxScroll > 0 then
        scrollBar:Show()
    else
        scrollBar:Hide()
        if scrollBar.mkaSetScrollValue then
            scrollBar.mkaSetScrollValue(0)
        elseif scrollBar.SetValue then
            scrollBar:SetValue(0)
        end
        scrollFrame:SetVerticalScroll(0)
    end

    content:SetWidth(math.max((scrollFrame:GetWidth() or 0), 1))
end

local function GetStartupTabOptionText(value)
    for _, option in ipairs(STARTUP_TAB_OPTIONS) do
        if option.value == value then
            return TRANSLATE(option.text)
        end
    end

    return TRANSLATE("TAB.GROUP")
end

local function GetAutoOpenCompletedRunOptionText(value)
    for _, option in ipairs(AUTO_OPEN_COMPLETED_RUN_OPTIONS) do
        if option.value == value then
            return TRANSLATE(option.text)
        end
    end

    return TRANSLATE("OPTIONS.AUTO_OPEN_OFF")
end

local function IsAutoOpenCompletedRunTab(value)
    if value == "off" then
        return false
    end

    for _, option in ipairs(AUTO_OPEN_COMPLETED_RUN_OPTIONS) do
        if option.value == value then
            return true
        end
    end

    return false
end

function MKA:GetStartupTabOptionText(value)
    return GetStartupTabOptionText(value)
end

function MKA:GetStartupTab()
    local value = self.db and self.db.profile and self.db.profile.startupTab or "group"
    for _, option in ipairs(STARTUP_TAB_OPTIONS) do
        if option.value == value then
            return value
        end
    end

    return "group"
end

function MKA:SetStartupTab(value)
    if not self.db or not self.db.profile then
        return
    end

    local normalizedValue = "group"
    for _, option in ipairs(STARTUP_TAB_OPTIONS) do
        if option.value == value then
            normalizedValue = value
            break
        end
    end

    if self.db.profile.startupTab == normalizedValue then
        return
    end

    self.db.profile.startupTab = normalizedValue

    if self.IsMainWindowShown and self:IsMainWindowShown() then
        self:RefreshMainWindow("SetStartupTab", normalizedValue)
    end
end

function MKA:GetRightClickStartupTab()
    local value = self.db and self.db.profile and self.db.profile.rightClickStartupTab or "meetingstone"
    for _, option in ipairs(STARTUP_TAB_OPTIONS) do
        if option.value == value then
            return value
        end
    end

    return "meetingstone"
end

function MKA:SetRightClickStartupTab(value)
    if not self.db or not self.db.profile then
        return
    end

    local normalizedValue = "meetingstone"
    for _, option in ipairs(STARTUP_TAB_OPTIONS) do
        if option.value == value then
            normalizedValue = value
            break
        end
    end

    if self.db.profile.rightClickStartupTab == normalizedValue then
        return
    end

    self.db.profile.rightClickStartupTab = normalizedValue

    if self.IsMainWindowShown and self:IsMainWindowShown() then
        self:RefreshMainWindow("SetRightClickStartupTab", normalizedValue)
    end
end

function MKA:GetAutoOpenOnCompletedRunTab()
    local value = self.db and self.db.profile and self.db.profile.autoOpenOnCompletedRun or "off"
    if value == true then
        local startupTab = self:GetStartupTab()
        return IsAutoOpenCompletedRunTab(startupTab) and startupTab or "group"
    end
    if value == false then
        return "off"
    end

    for _, option in ipairs(AUTO_OPEN_COMPLETED_RUN_OPTIONS) do
        if option.value == value then
            return value
        end
    end

    return "off"
end

function MKA:SetAutoOpenOnCompletedRunTab(value)
    if not self.db or not self.db.profile then
        return
    end

    local normalizedValue = "off"
    for _, option in ipairs(AUTO_OPEN_COMPLETED_RUN_OPTIONS) do
        if option.value == value then
            normalizedValue = value
            break
        end
    end

    if self.db.profile.autoOpenOnCompletedRun == normalizedValue then
        return
    end

    self.db.profile.autoOpenOnCompletedRun = normalizedValue

    if self.IsMainWindowShown and self:IsMainWindowShown() then
        self:RefreshMainWindow("SetAutoOpenOnCompletedRunTab", normalizedValue)
    end
end

local function FormatStartupTabTooltipLine(formatKey, tabText)
    local format = TRANSLATE(formatKey)
    if type(format) ~= "string" or format == "" or format == formatKey then
        format = "%s"
    end

    local ok, text = pcall(string.format, format, tabText)
    if ok and type(text) == "string" then
        return text
    end
    return tabText
end

function MKA:GetStartupClickTooltipLine(mouseButton)
    local isRightButton = mouseButton == "RightButton" or mouseButton == "right"
    local tabValue = isRightButton and self:GetRightClickStartupTab() or self:GetStartupTab()
    return FormatStartupTabTooltipLine(
        isRightButton and "TOOLTIP.OPEN_RIGHT_TAB_FORMAT" or "TOOLTIP.OPEN_LEFT_TAB_FORMAT",
        GetStartupTabOptionText(tabValue)
    )
end

local function SnapshotValue(value)
    if value == nil then
        return ""
    end

    return tostring(value)
end

local function CreateOptionsSectionTitle(parent, name, text)
    local createTitle = core.CreatePlayerInfoSectionTitle
    local title, titleBg, titleText
    if createTitle then
        title, titleBg, titleText = createTitle(parent, name, TRANSLATE(text))
    else
        title = CreateFrame("Frame", name, parent)
        title:SetHeight(40)
        titleText = title:CreateFontString(nil, "OVERLAY", "GameFontNormal")
        titleText:SetPoint("LEFT", title, "LEFT", 14, 0)
        titleText:SetText(TRANSLATE(text))
    end
    return title, titleBg, titleText
end

local function GetOptionsTacticalDungeonGroups(owner)
    local sourceGroups = owner and owner.GetCurrentSeasonDungeonGroups and owner:GetCurrentSeasonDungeonGroups() or {}
    local groups = {}
    if type(sourceGroups) ~= "table" then
        return groups
    end

    for _, dungeonGroup in ipairs(sourceGroups) do
        local mapID = tonumber(dungeonGroup and dungeonGroup.mapID)
        if mapID then
            groups[#groups + 1] = {
                mapID = mapID,
                dungeonName = dungeonGroup.dungeonName
                    or (owner.GetLocalizedDungeonName and owner:GetLocalizedDungeonName(mapID))
                    or TRANSLATE("DUNGEON.UNKNOWN"),
            }
        end
    end

    return groups
end

local function CreateOptionsTacticalAnnouncementGetter(mapID)
    return function(target)
        return target:GetDungeonTeleportTacticalAnnouncement(mapID)
    end
end

local function CreateOptionsTacticalAnnouncementSetter(mapID)
    return function(target, value)
        target:SetDungeonTeleportTacticalAnnouncement(mapID, value)
    end
end

local function ConfigureOptionsTacticalAnnouncementActions(input, confirmButton, clearButton)
    if not input then
        return
    end

    input.mkaRefreshActionButtons = function(self)
        local currentValue = self.mkaGetCurrentText and self:mkaGetCurrentText() or ""
        local savedValue = self.mkaGetSavedText and self:mkaGetSavedText() or ""
        SetOptionsActionButtonEnabled(confirmButton, currentValue ~= savedValue)
        SetOptionsActionButtonEnabled(clearButton, currentValue ~= "")
    end

    if confirmButton then
        confirmButton:SetScript("OnClick", function(button)
            if button.mkaOptionsActionEnabled == false then
                return
            end
            if input.mkaCommitText then
                input:mkaCommitText()
            end
            if input.mkaEditBox then
                input.mkaEditBox:ClearFocus()
            end
        end)
    end

    if clearButton then
        clearButton:SetScript("OnClick", function(button)
            if button.mkaOptionsActionEnabled == false then
                return
            end
            if input.mkaEditBox then
                input.mkaEditBox:SetText("")
                input.mkaEditBox:ClearFocus()
            end
            if input.mkaCommitText then
                input:mkaCommitText()
            end
        end)
    end

    input:mkaRefreshActionButtons()
end

local function ResetOptionsTabDefaults(owner)
    if not (owner and owner.db and owner.db.profile) then
        return
    end

    local profile = owner.db.profile
    profile.minimap = profile.minimap or {}
    profile.minimap.hide = false
    profile.microMenuButton = true
    profile.startupTab = "group"
    profile.rightClickStartupTab = "meetingstone"
    profile.findGroup = nil
    profile.autoOpenOnCompletedRun = "off"
    profile.dungeonTeleportAnnouncement = true
    profile.dungeonTeleportDialog = true
    profile.dungeonTeleportPrompt = true
    if owner.ResetDungeonTeleportCustomSettings then
        owner:ResetDungeonTeleportCustomSettings()
    end

    if owner.SetMinimapButtonVisible then
        owner:SetMinimapButtonVisible(true)
    end
    if owner.RefreshMicroMenuButtonAnchor then
        owner:RefreshMicroMenuButtonAnchor()
    end
    if owner.RefreshMicroMenuButtonState then
        owner:RefreshMicroMenuButtonState()
    end
    if owner.RefreshQueueStatusButtonOffset then
        owner:RefreshQueueStatusButtonOffset()
    end
    if owner.RefreshMainWindow then
        owner:RefreshMainWindow("ResetOptionsTabDefaults")
    end
end

local function CreateOptionsDropdown(parent, owner, config)
    config = config or {}
    local dropdown = CreateFrame("DropdownButton", config.name or nil, parent, "WowStyle1DropdownTemplate")
    dropdown:SetWidth(OPTIONS_DROPDOWN_WIDTH)
    if dropdown.SetHeight then
        dropdown:SetHeight(OPTIONS_DROPDOWN_HEIGHT)
    end
    local options = config.options or STARTUP_TAB_OPTIONS
    local getOptionText = config.getOptionText or GetStartupTabOptionText
    local getValue = config.getValue or function(target)
        return target:GetStartupTab()
    end
    local setValue = config.setValue or function(target, value)
        target:SetStartupTab(value)
    end
    dropdown:SetSelectionText(function()
        return getOptionText(getValue(owner))
    end)

    dropdown:SetupMenu(function(_, rootDescription)
        rootDescription:SetTag(config.menuTag or "MENU_MKA_STARTUP_TAB")
        local selectedValue = getValue(owner)
        for _, option in ipairs(options) do
            rootDescription:CreateRadio(getOptionText(option.value), function(data)
                return data.value == selectedValue
            end, function(data)
                setValue(owner, data.value)
                if dropdown.SetSelectionText then
                    dropdown:SetSelectionText(function()
                        return getOptionText(getValue(owner))
                    end)
                end
                if dropdown.GenerateMenu then
                    dropdown:GenerateMenu()
                end
                if dropdown.Update then
                    dropdown:Update()
                end
            end, option)
        end
    end)

    return dropdown
end

function OptionsTab.Build(owner, tabGroup)
    local optionsFrame = CreateFrame("Frame", "MKAOptionsPanel", UIParent)
    optionsFrame.mkaFillDecorFrame = true
    optionsFrame.mkaDecorFrameInsetLeft = 24
    optionsFrame.mkaDecorFrameInsetTop = 4
    optionsFrame.mkaDecorFrameInsetRight = 24
    optionsFrame.mkaDecorFrameInsetBottom = 10
    tabGroup:AddChild(optionsFrame)

    local scrollFrame, content, scrollBar
    local createScrollContainer = core.CreateNativeScrollContainer
    if createScrollContainer then
        scrollFrame, content, scrollBar = createScrollContainer(optionsFrame, function()
            UpdateOptionsScrollState(owner.widgets)
        end, 0)
    end

    if not (scrollFrame and content) then
        content = CreateFrame("Frame", nil, optionsFrame)
        content:SetAllPoints(optionsFrame)
    end
    content:SetHeight(OPTIONS_CONTENT_HEIGHT)

    if scrollBar and tabGroup and tabGroup.frame then
        scrollBar:ClearAllPoints()
        scrollBar:SetPoint("TOPRIGHT", tabGroup.frame, "TOPRIGHT", OPTIONS_SCROLLBAR_RIGHT_OFFSET, -4)
        scrollBar:SetPoint("BOTTOMRIGHT", tabGroup.frame, "BOTTOMRIGHT", OPTIONS_SCROLLBAR_RIGHT_OFFSET, 4)
        scrollBar:SetWidth(17)
    end

    local tacticalDungeonGroups = GetOptionsTacticalDungeonGroups(owner)

    local addonTitle, addonTitleBg, addonTitleText = CreateOptionsSectionTitle(content, "MKAOptionsAddonTitle", "OPTIONS.SECTION.ADDON")
    addonTitle:SetPoint("TOPLEFT", content, "TOPLEFT", 0, -OPTIONS_CONTENT_TOP_OFFSET)
    addonTitle:SetPoint("TOPRIGHT", content, "TOPRIGHT", 0, -OPTIONS_CONTENT_TOP_OFFSET)

    local defaultSettingsButton = CreateOptionsDefaultButton(addonTitle, function()
        ResetOptionsTabDefaults(owner)
    end)
    defaultSettingsButton:SetPoint("RIGHT", addonTitle, "RIGHT", -16, 0)
    defaultSettingsButton:SetFrameLevel((addonTitle:GetFrameLevel() or 0) + 4)

    local optionsSettingsPanelHeight = GetOptionsSettingsPanelHeight(core.MKA_OPTIONS_LAYOUT.panelRowCount)
    local optionsControlsHeight = optionsSettingsPanelHeight
        + core.MKA_OPTIONS_LAYOUT.panelSectionGap
        + core.MKA_OPTIONS_LAYOUT.previewPanelHeight

    local optionsControls = CreateFrame("Frame", nil, content)
    optionsControls:SetPoint("TOPLEFT", addonTitle, "BOTTOMLEFT", 0, -OPTIONS_TITLE_TO_CONTROLS_GAP)
    optionsControls:SetPoint("TOPRIGHT", addonTitle, "BOTTOMRIGHT", 0, -OPTIONS_TITLE_TO_CONTROLS_GAP)
    optionsControls:SetHeight(optionsControlsHeight)

    local optionsSettingsPanel = core.CreateOptionsSettingsPanel(optionsControls, core.MKA_OPTIONS_LAYOUT.panelRowCount)
    optionsSettingsPanel:SetPoint("TOPLEFT", optionsControls, "TOPLEFT", 0, 0)
    optionsSettingsPanel:SetPoint("TOPRIGHT", optionsControls, "TOPRIGHT", 0, 0)
    optionsSettingsPanel:SetHeight(optionsSettingsPanelHeight)
    local optionRows = optionsSettingsPanel.rows

    core.CreateOptionsRowLabel(optionRows[1], TRANSLATE("OPTIONS.MINIMAP_BUTTON_VISIBLE"))
    local minimapToggle = CreateOptionsNativeCheckButton(optionRows[1], function(value)
        owner:SetMinimapButtonVisible(value)
    end)
    core.AnchorOptionsRowControl(minimapToggle, optionRows[1])

    core.CreateOptionsRowLabel(optionRows[2], TRANSLATE("OPTIONS.MICRO_MENU_BUTTON_VISIBLE"))
    local microMenuToggle = CreateOptionsNativeCheckButton(optionRows[2], function(value)
        owner:SetMicroMenuButtonEnabled(value)
    end)
    core.AnchorOptionsRowControl(microMenuToggle, optionRows[2])

    core.CreateOptionsRowLabel(optionRows[3], TRANSLATE("OPTIONS.STARTUP_TAB"))
    local startupTabDropdown = CreateOptionsDropdown(optionRows[3], owner, {
        name = "MKAStartupTabDropdown",
        menuTag = "MENU_MKA_STARTUP_TAB",
        getValue = function(target)
            return target:GetStartupTab()
        end,
        setValue = function(target, value)
            target:SetStartupTab(value)
        end,
    })
    core.AnchorOptionsRowControl(startupTabDropdown, optionRows[3])

    core.CreateOptionsRowLabel(optionRows[4], TRANSLATE("OPTIONS.RIGHT_CLICK_STARTUP_TAB"))
    local rightClickStartupTabDropdown = CreateOptionsDropdown(optionRows[4], owner, {
        name = "MKARightClickStartupTabDropdown",
        menuTag = "MENU_MKA_RIGHT_CLICK_STARTUP_TAB",
        getValue = function(target)
            return target:GetRightClickStartupTab()
        end,
        setValue = function(target, value)
            target:SetRightClickStartupTab(value)
        end,
    })
    core.AnchorOptionsRowControl(rightClickStartupTabDropdown, optionRows[4])

    core.CreateOptionsRowLabel(optionRows[5], TRANSLATE("OPTIONS.AUTO_OPEN_AFTER_KEY"))
    local autoOpenDropdown = CreateOptionsDropdown(optionRows[5], owner, {
        name = "MKAAutoOpenCompletedRunDropdown",
        menuTag = "MENU_MKA_AUTO_OPEN_COMPLETED_RUN",
        options = AUTO_OPEN_COMPLETED_RUN_OPTIONS,
        getOptionText = GetAutoOpenCompletedRunOptionText,
        getValue = function(target)
            return target:GetAutoOpenOnCompletedRunTab()
        end,
        setValue = function(target, value)
            target:SetAutoOpenOnCompletedRunTab(value)
        end,
    })
    core.AnchorOptionsRowControl(autoOpenDropdown, optionRows[5])

    core.CreateOptionsRowLabel(optionRows[6], TRANSLATE("TELEPORT.PROMPT"))
    local teleportPromptToggle = CreateOptionsNativeCheckButton(optionRows[6], function(value)
        owner:SetDungeonTeleportPromptEnabled(value)
    end)
    local teleportPromptPreviewButton = CreateOptionsPromptPreviewButton(optionRows[6], function()
        if owner.ShowDungeonTeleportDialogTest then
            owner:ShowDungeonTeleportDialogTest()
        end
    end)
    AnchorOptionsPromptControls(optionRows[6], teleportPromptToggle, teleportPromptPreviewButton)

    core.CreateOptionsRowLabel(optionRows[7], TRANSLATE("TELEPORT.ANNOUNCEMENT"))
    local teleportAnnouncementToggle = CreateOptionsNativeCheckButton(optionRows[7], function(value)
        owner:SetDungeonTeleportAnnouncementEnabled(value)
    end)
    core.AnchorOptionsRowControl(teleportAnnouncementToggle, optionRows[7])

    core.CreateOptionsRowLabel(optionRows[8], TRANSLATE("TELEPORT.CUSTOM_TEXT"))
    local teleportDialogMessageBox = CreateOptionsTextInput(optionRows[8], owner, function(target)
        return target:GetDungeonTeleportDialogMessage()
    end, function(target, value)
        target:SetDungeonTeleportDialogMessage(value)
    end, OPTIONS_MESSAGE_INPUT_WIDTH)

    local teleportDialogMessageConfirmButton = CreateOptionsConfirmButton(optionRows[8], function()
        if teleportDialogMessageBox and teleportDialogMessageBox.mkaCommitText then
            teleportDialogMessageBox:mkaCommitText()
        end
        if teleportDialogMessageBox and teleportDialogMessageBox.mkaEditBox then
            teleportDialogMessageBox.mkaEditBox:ClearFocus()
        end
    end)
    core.AnchorOptionsMessageControls(optionRows[8], teleportDialogMessageBox, teleportDialogMessageConfirmButton)

    local optionsPreviewPanel = core.CreateOptionsPreviewPanel(optionsControls)
    optionsPreviewPanel:SetPoint("TOPLEFT", optionsSettingsPanel, "BOTTOMLEFT", 0, -core.MKA_OPTIONS_LAYOUT.panelSectionGap)
    optionsPreviewPanel:SetPoint("TOPRIGHT", optionsSettingsPanel, "BOTTOMRIGHT", 0, -core.MKA_OPTIONS_LAYOUT.panelSectionGap)
    optionsPreviewPanel:SetHeight(core.MKA_OPTIONS_LAYOUT.previewPanelHeight)

    local teleportDialogMessagePreview = core.CreateOptionsAnnouncementPreview(optionsPreviewPanel, owner)
    teleportDialogMessagePreview:ClearAllPoints()
    teleportDialogMessagePreview:SetPoint("TOPLEFT", optionsPreviewPanel, "TOPLEFT", 18, -30)
    teleportDialogMessagePreview:SetPoint("BOTTOMRIGHT", optionsPreviewPanel, "BOTTOMRIGHT", -18, 8)
    teleportDialogMessagePreview:mkaSetPreviewMessage(owner:GetDungeonTeleportDialogMessage())
    teleportDialogMessageBox.mkaOnTextChanged = function(_, value)
        teleportDialogMessagePreview:mkaSetPreviewMessage(value)
    end

    local tacticalTitle, tacticalTitleBg, tacticalTitleText = CreateOptionsSectionTitle(content, "MKAOptionsTacticalTitle", "TELEPORT.TACTICAL_ANNOUNCEMENT")
    tacticalTitle:SetPoint("TOPLEFT", optionsControls, "BOTTOMLEFT", 0, -OPTIONS_SECTION_GAP)
    tacticalTitle:SetPoint("TOPRIGHT", optionsControls, "BOTTOMRIGHT", 0, -OPTIONS_SECTION_GAP)

    local tacticalPanelHeight = GetOptionsSettingsPanelHeight(#tacticalDungeonGroups)
    local tacticalAnnouncementPanel = core.CreateOptionsSettingsPanel(content, #tacticalDungeonGroups)
    tacticalAnnouncementPanel:SetPoint("TOPLEFT", tacticalTitle, "BOTTOMLEFT", 0, -OPTIONS_TITLE_TO_CONTROLS_GAP)
    tacticalAnnouncementPanel:SetPoint("TOPRIGHT", tacticalTitle, "BOTTOMRIGHT", 0, -OPTIONS_TITLE_TO_CONTROLS_GAP)
    tacticalAnnouncementPanel:SetHeight(tacticalPanelHeight)

    local tacticalAnnouncementInputs = {}
    for index, dungeonGroup in ipairs(tacticalDungeonGroups) do
        local row = tacticalAnnouncementPanel.rows[index]
        local mapID = dungeonGroup.mapID
        core.CreateOptionsRowLabel(row, dungeonGroup.dungeonName or TRANSLATE("DUNGEON.UNKNOWN"))
        local tacticalConfirmButton = CreateOptionsConfirmButton(row)
        local tacticalClearButton = CreateOptionsClearButton(row)
        local tacticalInput = CreateOptionsTextInput(
            row,
            owner,
            CreateOptionsTacticalAnnouncementGetter(mapID),
            CreateOptionsTacticalAnnouncementSetter(mapID),
            OPTIONS_MESSAGE_INPUT_WIDTH,
            {
                maxBytes = OPTIONS_TACTICAL_MESSAGE_MAX_BYTES,
                maxLetters = OPTIONS_TACTICAL_MESSAGE_MAX_BYTES,
                placeholder = TRANSLATE("TELEPORT.TACTICAL_PLACEHOLDER"),
            }
        )
        tacticalInput.mkaMapID = mapID
        tacticalInput.mkaConfirmButton = tacticalConfirmButton
        tacticalInput.mkaClearButton = tacticalClearButton
        AnchorOptionsTacticalMessageControls(row, tacticalInput, tacticalConfirmButton, tacticalClearButton)
        ConfigureOptionsTacticalAnnouncementActions(tacticalInput, tacticalConfirmButton, tacticalClearButton)
        tacticalAnnouncementInputs[#tacticalAnnouncementInputs + 1] = tacticalInput
    end

    local addonTitleHeight = math.max(addonTitle:GetHeight() or 0, 40)
    local tacticalTitleHeight = math.max(tacticalTitle:GetHeight() or 0, 40)
    content.mkaOptionsContentHeight = math.max(
        OPTIONS_CONTENT_HEIGHT,
        OPTIONS_CONTENT_TOP_OFFSET
            + addonTitleHeight
            + OPTIONS_TITLE_TO_CONTROLS_GAP
            + optionsControlsHeight
            + OPTIONS_SECTION_GAP
            + tacticalTitleHeight
            + OPTIONS_TITLE_TO_CONTROLS_GAP
            + tacticalPanelHeight
            + OPTIONS_CONTENT_BOTTOM_PADDING
    )
    content:SetHeight(content.mkaOptionsContentHeight)

    owner.widgets = {
        tabGroup = owner.widgets and owner.widgets.tabGroup or tabGroup,
        root = optionsFrame,
        optionsFrame = optionsFrame,
        optionsScrollFrame = scrollFrame,
        optionsScrollBar = scrollBar,
        optionsContent = content,
        addonTitle = addonTitle,
        addonTitleBg = addonTitleBg,
        addonTitleText = addonTitleText,
        defaultSettingsButton = defaultSettingsButton,
        optionsControls = optionsControls,
        optionsSettingsPanel = optionsSettingsPanel,
        optionsPreviewPanel = optionsPreviewPanel,
        tacticalTitle = tacticalTitle,
        tacticalTitleBg = tacticalTitleBg,
        tacticalTitleText = tacticalTitleText,
        tacticalAnnouncementPanel = tacticalAnnouncementPanel,
        tacticalAnnouncementInputs = tacticalAnnouncementInputs,
        minimapToggle = minimapToggle,
        microMenuToggle = microMenuToggle,
        startupTabDropdown = startupTabDropdown,
        rightClickStartupTabDropdown = rightClickStartupTabDropdown,
        autoOpenDropdown = autoOpenDropdown,
        teleportAnnouncementToggle = teleportAnnouncementToggle,
        teleportPromptToggle = teleportPromptToggle,
        teleportPromptPreviewButton = teleportPromptPreviewButton,
        teleportDialogToggle = teleportAnnouncementToggle,
        teleportDialogMessageBox = teleportDialogMessageBox,
        teleportDialogMessageConfirmButton = teleportDialogMessageConfirmButton,
        teleportDialogMessagePreview = teleportDialogMessagePreview,
    }
    owner.tabViews = owner.tabViews or {}
    owner.tabViews.options = owner.widgets

    if core.ApplyThemeToTabGroup then
        core.ApplyThemeToTabGroup(tabGroup)
    end
    if core.ApplyFontToFrame then
        core.ApplyFontToFrame(optionsFrame)
    end

    C_Timer.After(0, function()
        if core.FinalizeFlatTabLayout then
            core.FinalizeFlatTabLayout(tabGroup)
        end
        if owner.activeTab == "options" and owner.widgets and owner.widgets.optionsContent == content and scrollFrame then
            UpdateOptionsScrollState(owner.widgets)
        end
    end)

    owner:RefreshMainWindow("BuildSelectedTab", "options")
end

function OptionsTab.Refresh(owner, widgets)
    if not widgets or not widgets.minimapToggle then
        return
    end

    widgets.minimapToggle:SetChecked(not owner.db.profile.minimap.hide)
    if widgets.microMenuToggle then
        widgets.microMenuToggle:SetChecked(owner:IsMicroMenuButtonEnabled())
    end
    if widgets.teleportAnnouncementToggle then
        widgets.teleportAnnouncementToggle:SetChecked(owner:IsDungeonTeleportAnnouncementEnabled())
    elseif widgets.teleportDialogToggle then
        widgets.teleportDialogToggle:SetChecked(owner:IsDungeonTeleportAnnouncementEnabled())
    end
    if widgets.teleportPromptToggle then
        widgets.teleportPromptToggle:SetChecked(owner:IsDungeonTeleportPromptEnabled())
    end
    if widgets.teleportDialogMessageBox and widgets.teleportDialogMessageBox.mkaSetDisplayText then
        widgets.teleportDialogMessageBox:mkaSetDisplayText(owner:GetDungeonTeleportDialogMessage())
    end
    if widgets.teleportDialogMessagePreview and widgets.teleportDialogMessagePreview.mkaSetPreviewMessage then
        local previewValue = owner:GetDungeonTeleportDialogMessage()
        if widgets.teleportDialogMessageBox and widgets.teleportDialogMessageBox.mkaEditBox
            and widgets.teleportDialogMessageBox.mkaEditBox.HasFocus
            and widgets.teleportDialogMessageBox.mkaEditBox:HasFocus()
        then
            previewValue = widgets.teleportDialogMessageBox.mkaEditBox:GetText() or previewValue
        end
        widgets.teleportDialogMessagePreview:mkaSetPreviewMessage(previewValue)
    end
    if widgets.tacticalAnnouncementInputs then
        for _, tacticalInput in ipairs(widgets.tacticalAnnouncementInputs) do
            if tacticalInput.mkaMapID and tacticalInput.mkaSetDisplayText then
                tacticalInput:mkaSetDisplayText(owner:GetDungeonTeleportTacticalAnnouncement(tacticalInput.mkaMapID))
            end
        end
    end
    if widgets.startupTabDropdown then
        if widgets.startupTabDropdown.GenerateMenu then
            widgets.startupTabDropdown:GenerateMenu()
        end
        if widgets.startupTabDropdown.SetSelectionText then
            widgets.startupTabDropdown:SetSelectionText(function()
                return GetStartupTabOptionText(owner:GetStartupTab())
            end)
        end
        if widgets.startupTabDropdown.Update then
            widgets.startupTabDropdown:Update()
        end
    end
    if widgets.rightClickStartupTabDropdown then
        if widgets.rightClickStartupTabDropdown.GenerateMenu then
            widgets.rightClickStartupTabDropdown:GenerateMenu()
        end
        if widgets.rightClickStartupTabDropdown.SetSelectionText then
            widgets.rightClickStartupTabDropdown:SetSelectionText(function()
                return GetStartupTabOptionText(owner:GetRightClickStartupTab())
            end)
        end
        if widgets.rightClickStartupTabDropdown.Update then
            widgets.rightClickStartupTabDropdown:Update()
        end
    end
    if widgets.autoOpenDropdown then
        if widgets.autoOpenDropdown.GenerateMenu then
            widgets.autoOpenDropdown:GenerateMenu()
        end
        if widgets.autoOpenDropdown.SetSelectionText then
            widgets.autoOpenDropdown:SetSelectionText(function()
                return GetAutoOpenCompletedRunOptionText(owner:GetAutoOpenOnCompletedRunTab())
            end)
        end
        if widgets.autoOpenDropdown.Update then
            widgets.autoOpenDropdown:Update()
        end
    end
    UpdateOptionsScrollState(widgets)
end

function MKA:BuildOptionsTabSnapshot()
    local minimapHidden = self.db and self.db.profile and self.db.profile.minimap and self.db.profile.minimap.hide and "1" or "0"
    local warbandTag = self:GetWarbandTag() or ""
    local startupTab = self:GetStartupTab()
    local rightClickStartupTab = self:GetRightClickStartupTab()
    local autoOpenOnCompletedRun = self:GetAutoOpenOnCompletedRunTab()
    local dungeonTeleportAnnouncement = self:IsDungeonTeleportAnnouncementEnabled() and "1" or "0"
    local dungeonTeleportPrompt = self:IsDungeonTeleportPromptEnabled() and "1" or "0"
    local microMenuButton = self:IsMicroMenuButtonEnabled() and "1" or "0"
    local dungeonTeleportDialogMessage = self:GetDungeonTeleportDialogMessage() or ""
    local tacticalParts = {}
    for _, dungeonGroup in ipairs(GetOptionsTacticalDungeonGroups(self)) do
        local mapID = dungeonGroup.mapID
        tacticalParts[#tacticalParts + 1] = table.concat({
            SnapshotValue(mapID),
            SnapshotValue(self:GetDungeonTeleportTacticalAnnouncement(mapID)),
        }, "~")
    end

    return table.concat({
        "options",
        minimapHidden,
        microMenuButton,
        warbandTag,
        startupTab,
        rightClickStartupTab,
        autoOpenOnCompletedRun,
        dungeonTeleportAnnouncement,
        dungeonTeleportPrompt,
        dungeonTeleportDialogMessage,
        table.concat(tacticalParts, "||"),
    }, "|")
end
