local _, addonTable = ...
local WipedQoL = addonTable.WipedQoL
local TRANSLATE = addonTable.TRANSLATE or function(msg) return msg end

local UI = addonTable.UI or {}
addonTable.UI = UI

local TAB_ORDER = {
    { key = "general", label = "General" },
    { key = "banking", label = "Banking" },
    { key = "questing", label = "Questing" },
    { key = "rolling", label = "Rolling" },
    { key = "sounds", label = "Sounds" },
    { key = "gameplay", label = "Gameplay" },
    { key = "datapanel", label = "Datapanel" },
    { key = "weeklyTasks", label = "Reminder" },
    { key = "skinning", label = "Skinning" },
    { key = "addon", label = "Addon" },
}
local TAB_BUTTON_WIDTH = 62
local TAB_BUTTON_SPACING = 3

local function GetProfile()
    return WipedQoL.db.profile
end

function UI.ApplyFont(fontString, size, flags)
    if addonTable.ApplyFont then
        addonTable.ApplyFont(fontString, size, flags)
    end
end

function UI.SkinFrame(frame, opts)
    if addonTable.SkinFrame then
        addonTable.SkinFrame(frame, opts)
    end
end

function UI.SkinButton(button)
    if addonTable.SkinButton then
        addonTable.SkinButton(button)
    end
end

function UI.SkinCloseButton(button)
    if addonTable.SkinCloseButton then
        addonTable.SkinCloseButton(button)
    elseif addonTable.SkinButton then
        addonTable.SkinButton(button)
    end
end

function UI.SkinScrollBar(scrollBar)
    if addonTable.SkinScrollBar then
        addonTable.SkinScrollBar(scrollBar)
    end
end

function UI.CreateLabel(parent, text, size)
    local label = parent:CreateFontString(nil, "ARTWORK", "GameFontNormal")
    label:SetText(text)
    label:SetTextColor(1, 0.82, 0)
    label:SetJustifyH("LEFT")
    UI.ApplyFont(label, size or 12, "")
    return label
end

function UI.SetSectionDescriptionInsets(section, leftInset, rightInset)
    if not section or not section.description then
        return
    end

    section._descriptionLeftInset = tonumber(leftInset) or 10
    section._descriptionRightInset = tonumber(rightInset) or 10

    section.description:ClearAllPoints()
    section.description:SetPoint(
        "TOPLEFT",
        section,
        "TOPLEFT",
        section._descriptionLeftInset,
        -31
    )
    section.description:SetWidth(math.max(
        (section:GetWidth() or 0) - section._descriptionLeftInset - section._descriptionRightInset,
        1
    ))
end

function UI.CreateSection(parent, titleText, descriptionText)
    local section = CreateFrame("Frame", nil, parent, "BackdropTemplate")
    UI.SkinFrame(section, { light = true })

    section.title = UI.CreateLabel(section, titleText, 12)
    section.title:SetPoint("TOPLEFT", section, "TOPLEFT", 10, -8)
    section.title:SetPoint("TOPRIGHT", section, "TOPRIGHT", -10, -8)
    section.title:SetJustifyH("LEFT")
    section.title:SetTextColor(1, 1, 1, 0.92)

    section.description = UI.CreateLabel(section, descriptionText or "", 11)
    section.description:SetJustifyH("LEFT")
    section.description:SetJustifyV("TOP")
    section.description:SetTextColor(0.62, 0.62, 0.62)
    section.description:SetWordWrap(true)
    if section.description.SetNonSpaceWrap then
        section.description:SetNonSpaceWrap(true)
    end

    section:HookScript("OnSizeChanged", function(self)
        UI.SetSectionDescriptionInsets(
            self,
            self._descriptionLeftInset,
            self._descriptionRightInset
        )
    end)
    UI.SetSectionDescriptionInsets(section, 10, 10)

    return section
end

function UI.CreateSectionToggle(section, getter, setter)
    local toggle = UI.CreateCheckBox(section, "", getter, setter)
    toggle:SetSize(38, 24)
    toggle:SetPoint("TOPRIGHT", section, "TOPRIGHT", -10, -3)

    section.title:ClearAllPoints()
    section.title:SetPoint("TOPLEFT", section, "TOPLEFT", 10, -8)
    section.title:SetPoint("TOPRIGHT", section, "TOPRIGHT", -58, -8)
    return toggle
end

function UI.GetClassColor()
    if addonTable.GetClassColor then
        return addonTable.GetClassColor()
    end

    local classTag
    if UnitClass then
        _, classTag = UnitClass("player")
    end

    if classTag == "PRIEST" then
        return 1, 0.49, 0.04
    end

    local colors = CUSTOM_CLASS_COLORS or RAID_CLASS_COLORS
    local color = colors and classTag and colors[classTag]

    if color then
        return color.r or 1, color.g or 0.82, color.b or 0
    end

    return 1, 0.82, 0
end

function UI.SetAddonColor(target, method, brightness, alpha)
    if addonTable.SetAddonColorTarget then
        addonTable.SetAddonColorTarget(target, method, brightness, alpha)
        return
    end

    if not target or not target[method] then
        return
    end
    local r, g, b = UI.GetClassColor()
    local multiplier = tonumber(brightness) or 1
    target[method](target, r * multiplier, g * multiplier, b * multiplier, tonumber(alpha) or 1)
end

local function CreateEdge(parent, fromPoint, fromFrame, fromRelativePoint, fromX, fromY, toPoint, toFrame, toRelativePoint, toX, toY, thickness)
    local edge = parent:CreateTexture(nil, "BORDER")
    edge:SetTexture("Interface\\Buttons\\WHITE8X8")
    edge:SetPoint(fromPoint, fromFrame, fromRelativePoint, fromX, fromY)
    edge:SetPoint(toPoint, toFrame, toRelativePoint, toX, toY)

    local isVertical = (fromPoint:find("LEFT") and toPoint:find("LEFT"))
        or (fromPoint:find("RIGHT") and toPoint:find("RIGHT"))

    if isVertical then
        edge:SetWidth(thickness or 1)
    else
        edge:SetHeight(thickness or 1)
    end

    return edge
end

local function HideTextureRegion(region)
    if not region or not region.GetObjectType or region:GetObjectType() ~= "Texture" then
        return
    end

    if region.SetTexture then
        region:SetTexture(nil)
    end
    if region.SetAlpha then
        region:SetAlpha(0)
    end
    if region.Hide then
        region:Hide()
    end
end

local function StripDropdownMenuFrame(frame)
    if not frame or frame._wqolDropdownMenuStripped then
        return
    end

    for _, region in ipairs({ frame:GetRegions() }) do
        HideTextureRegion(region)
    end

    if frame.NineSlice then
        frame.NineSlice:Hide()
    end

    frame._wqolDropdownMenuStripped = true
end

local function EnsureDropdownMenuLayers(frame)
    if not frame or frame._wqolDropdownMenuLayersReady then
        return
    end

    local background = frame:CreateTexture(nil, "BACKGROUND", nil, -8)
    background:SetPoint("TOPLEFT", frame, "TOPLEFT", 0, 0)
    background:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", 0, 0)
    background:SetTexture("Interface\\Buttons\\WHITE8X8")
    background:SetVertexColor(0, 0, 0, 0.88)
    frame._wqolDropdownMenuBackground = background

    local inner = frame:CreateTexture(nil, "BORDER", nil, -6)
    inner:SetPoint("TOPLEFT", frame, "TOPLEFT", 1, -1)
    inner:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -1, 1)
    inner:SetTexture("Interface\\Buttons\\WHITE8X8")
    inner:SetVertexColor(1, 1, 1, 0.035)
    frame._wqolDropdownMenuInner = inner

    frame._wqolDropdownMenuBorderEdges = {
        top = CreateEdge(frame, "TOPLEFT", frame, "TOPLEFT", 0, 0, "TOPRIGHT", frame, "TOPRIGHT", 0, 0, 1),
        bottom = CreateEdge(frame, "BOTTOMLEFT", frame, "BOTTOMLEFT", 0, 0, "BOTTOMRIGHT", frame, "BOTTOMRIGHT", 0, 0, 1),
        left = CreateEdge(frame, "TOPLEFT", frame, "TOPLEFT", 0, 0, "BOTTOMLEFT", frame, "BOTTOMLEFT", 0, 0, 1),
        right = CreateEdge(frame, "TOPRIGHT", frame, "TOPRIGHT", 0, 0, "BOTTOMRIGHT", frame, "BOTTOMRIGHT", 0, 0, 1),
    }

    for _, edge in pairs(frame._wqolDropdownMenuBorderEdges) do
        edge:SetVertexColor(0, 0, 0, 0)
    end

    frame._wqolDropdownMenuLayersReady = true
end

local function ApplyThemeToDropdownMenu(level)
    level = level or 1

    local listFrame = _G["DropDownList" .. level]
    if not listFrame then
        return
    end

    StripDropdownMenuFrame(listFrame)
    EnsureDropdownMenuLayers(listFrame)

    if listFrame._wqolDropdownMenuBackground then
        listFrame._wqolDropdownMenuBackground:Show()
        listFrame._wqolDropdownMenuBackground:SetVertexColor(0, 0, 0, 0.88)
    end

    if listFrame._wqolDropdownMenuInner then
        listFrame._wqolDropdownMenuInner:Show()
        listFrame._wqolDropdownMenuInner:SetVertexColor(1, 1, 1, 0.035)
    end

    for _, edge in pairs(listFrame._wqolDropdownMenuBorderEdges or {}) do
        edge:Show()
        edge:SetVertexColor(0, 0, 0, 0)
    end

    for _, suffix in ipairs({ "Backdrop", "MenuBackdrop" }) do
        local backdropFrame = _G["DropDownList" .. level .. suffix]
        if backdropFrame then
            StripDropdownMenuFrame(backdropFrame)
            backdropFrame:Hide()
        end
    end

    if listFrame.SetBackdrop then
        listFrame:SetBackdrop(nil)
    end
end

local CHECKBOX_CHECK_SIZE = 14
local CHECKBOX_FILL_DURATION = 0.12
local CHECKBOX_PULSE_DURATION = 0.22
local CHECKBOX_UNCHECK_DURATION = 0.18
local CHECKBOX_PULSE_SIZE = 18
local CHECKBOX_HOVER_SIZE = 6

local function StopCheckBoxAnimation(button)
    if not button then
        return
    end

    button:SetScript("OnUpdate", nil)
    button._wqolCheckAnimation = nil

    if button._wqolCheckTexture then
        button._wqolCheckTexture:SetSize(CHECKBOX_CHECK_SIZE, CHECKBOX_CHECK_SIZE)
        button._wqolCheckTexture:SetAlpha(1)
    end
end

local function StartCheckBoxPulse(button)
    local check = button and button._wqolCheckTexture
    if not check then
        return
    end

    button._wqolCheckAnimation = {
        mode = "pulse",
        elapsed = 0,
    }

    button:SetScript("OnUpdate", function(self, delta)
        local animation = self._wqolCheckAnimation
        if not animation then
            return
        end

        animation.elapsed = (animation.elapsed or 0) + delta
        local progress = math.min(animation.elapsed / CHECKBOX_PULSE_DURATION, 1)
        local pulse = math.sin(progress * math.pi)
        local size = CHECKBOX_CHECK_SIZE + ((CHECKBOX_PULSE_SIZE - CHECKBOX_CHECK_SIZE) * pulse)

        check:Show()
        check:SetAlpha(1)
        check:SetSize(size, size)

        if progress >= 1 then
            StopCheckBoxAnimation(self)
            check:Show()
        end
    end)
end

local function StartCheckBoxCheckAnimation(button)
    local check = button and button._wqolCheckTexture
    if not check then
        return
    end

    StopCheckBoxAnimation(button)
    button._wqolCheckAnimation = {
        mode = "fill",
        elapsed = 0,
    }
    check:SetAlpha(1)
    check:SetSize(0, 0)
    check:Show()

    button:SetScript("OnUpdate", function(self, delta)
        local animation = self._wqolCheckAnimation
        if not animation then
            return
        end

        animation.elapsed = (animation.elapsed or 0) + delta
        local progress = math.min(animation.elapsed / CHECKBOX_FILL_DURATION, 1)
        local size = CHECKBOX_CHECK_SIZE * progress

        check:SetAlpha(1)
        check:SetSize(size, size)
        check:Show()

        if progress >= 1 then
            StartCheckBoxPulse(self)
        end
    end)
end

local function StartCheckBoxUncheckAnimation(button)
    local check = button and button._wqolCheckTexture
    if not check then
        return
    end

    StopCheckBoxAnimation(button)
    button._wqolCheckAnimation = {
        mode = "uncheck",
        elapsed = 0,
    }
    check:SetAlpha(1)
    check:SetSize(CHECKBOX_CHECK_SIZE, CHECKBOX_CHECK_SIZE)
    check:Show()

    button:SetScript("OnUpdate", function(self, delta)
        local animation = self._wqolCheckAnimation
        if not animation then
            return
        end

        animation.elapsed = (animation.elapsed or 0) + delta
        local progress = math.min(animation.elapsed / CHECKBOX_UNCHECK_DURATION, 1)
        local pop = math.sin(math.min(progress / 0.45, 1) * math.pi)
        local size = CHECKBOX_CHECK_SIZE + (4 * pop)

        check:SetSize(size, size)
        check:SetAlpha(1 - progress)
        check:Show()

        if progress >= 1 then
            StopCheckBoxAnimation(self)
            check:Hide()
            check:SetAlpha(1)
        end
    end)
end

function UI.CreateToggleSwitch(parent, width, height)
    width = width or 38
    height = height or 18

    local button = CreateFrame("Button", nil, parent)
    button:SetSize(width, height)

    local knobSize = math.max(height - 4, 8)
    local knobTravel = math.max(width - knobSize - 4, 0)

    local track = CreateFrame("Frame", nil, button, "BackdropTemplate")
    track:SetAllPoints(button)
    UI.SkinFrame(track, { light = true })

    local fill = track:CreateTexture(nil, "ARTWORK", nil, 1)
    fill:SetPoint("TOPLEFT", track, "TOPLEFT", 1, -1)
    fill:SetPoint("BOTTOMRIGHT", track, "BOTTOMRIGHT", -1, 1)
    fill:SetTexture("Interface\\Buttons\\WHITE8X8")

    local border = track:CreateTexture(nil, "OVERLAY", nil, 1)
    border:SetPoint("TOPLEFT", track, "TOPLEFT", 0, 0)
    border:SetPoint("BOTTOMRIGHT", track, "BOTTOMRIGHT", 0, 0)
    border:SetTexture("Interface\\Buttons\\WHITE8X8")
    border:SetVertexColor(1, 1, 1, 0.08)

    local inner = track:CreateTexture(nil, "OVERLAY", nil, 2)
    inner:SetPoint("TOPLEFT", track, "TOPLEFT", 1, -1)
    inner:SetPoint("BOTTOMRIGHT", track, "BOTTOMRIGHT", -1, 1)
    inner:SetTexture("Interface\\Buttons\\WHITE8X8")

    local knob = track:CreateTexture(nil, "OVERLAY", nil, 3)
    knob:SetSize(knobSize, knobSize)
    knob:SetTexture("Interface\\Buttons\\WHITE8X8")
    button._wqolToggleKnob = knob

    local function SetToggleProgress(progress, hovered)
        progress = math.max(0, math.min(progress or 0, 1))
        button._wqolToggleProgress = progress

        local cr, cg, cb = UI.GetClassColor()
        local offR, offG, offB = 0.11, 0.11, 0.11
        local r = offR + ((cr * 0.62) - offR) * progress
        local g = offG + ((cg * 0.62) - offG) * progress
        local b = offB + ((cb * 0.62) - offB) * progress

        fill:SetVertexColor(r, g, b, 0.92)
        inner:SetVertexColor(1, 1, 1, progress > 0.5 and 0.055 or 0.025)
        border:SetVertexColor(hovered and 1 or cr, hovered and 1 or cg, hovered and 1 or cb, hovered and 0.24 or (0.22 + (0.26 * progress)))
        knob:SetVertexColor(0.94, 0.94, 0.94, 1)

        knob:ClearAllPoints()
        knob:SetPoint("LEFT", track, "LEFT", 2 + (knobTravel * progress), 0)
    end

    local function StopToggleAnimation()
        button:SetScript("OnUpdate", nil)
        button._wqolToggleAnimation = nil
    end

    local function AnimateToggle(fromProgress, toProgress)
        StopToggleAnimation()
        button._wqolToggleAnimation = {
            from = fromProgress,
            to = toProgress,
            elapsed = 0,
            duration = 0.14,
        }

        button:SetScript("OnUpdate", function(self, delta)
            local animation = self._wqolToggleAnimation
            if not animation then
                return
            end

            animation.elapsed = (animation.elapsed or 0) + delta
            local progress = math.min(animation.elapsed / animation.duration, 1)
            local eased = 1 - ((1 - progress) * (1 - progress))
            local value = animation.from + ((animation.to - animation.from) * eased)
            SetToggleProgress(value, self:IsMouseOver())

            if progress >= 1 then
                StopToggleAnimation()
                SetToggleProgress(animation.to, self:IsMouseOver())
            end
        end)
    end

    local function GetDisplayProgress()
        if button._wqolToggleMixed == true then
            return 0.5
        end

        return button._wqolToggleChecked and 1 or 0
    end

    function button:RefreshColor()
        SetToggleProgress(GetDisplayProgress(), self:IsMouseOver())
    end

    function button:SetChecked(checked, animate)
        local oldValue = self._wqolToggleChecked == true
        local newValue = checked == true
        local targetProgress = newValue and 1 or 0
        self._wqolToggleChecked = newValue
        self._wqolToggleMixed = nil

        if animate and (oldValue ~= newValue or self._wqolToggleProgress ~= targetProgress) then
            AnimateToggle(tonumber(self._wqolToggleProgress) or (oldValue and 1 or 0), targetProgress)
        elseif animate and self._wqolToggleAnimation and self._wqolToggleAnimation.to == targetProgress then
            return
        else
            StopToggleAnimation()
            SetToggleProgress(targetProgress, self:IsMouseOver())
        end
    end

    function button:SetMixed(mixed, animate)
        if mixed ~= true then
            self._wqolToggleMixed = nil
            self:Refresh()
            return
        end

        self._wqolToggleMixed = true
        self._wqolToggleChecked = false

        if animate and self._wqolToggleAnimation and self._wqolToggleAnimation.to == 0.5 then
            return
        elseif animate and math.abs((tonumber(self._wqolToggleProgress) or 0) - 0.5) > 0.001 then
            AnimateToggle(tonumber(self._wqolToggleProgress) or 0, 0.5)
        else
            StopToggleAnimation()
            SetToggleProgress(0.5, self:IsMouseOver())
        end
    end

    function button:GetChecked()
        return self._wqolToggleChecked == true
    end

    function button:Refresh()
        StopToggleAnimation()
        SetToggleProgress(GetDisplayProgress(), self:IsMouseOver())
    end

    button:SetScript("OnEnter", function(self)
        SetToggleProgress(self._wqolToggleAnimation and self._wqolToggleProgress or GetDisplayProgress(), true)
    end)
    button:SetScript("OnLeave", function(self)
        SetToggleProgress(self._wqolToggleAnimation and self._wqolToggleProgress or GetDisplayProgress(), false)
    end)
    button:SetScript("OnShow", function(self)
        self:Refresh()
    end)
    button:SetChecked(false, false)

    return button
end

function UI.CreateCheckBox(parent, labelText, getter, setter)
    local button = CreateFrame("Button", nil, parent)
    button:SetSize(300, 24)

    local toggle = UI.CreateToggleSwitch(button, 38, 18)
    toggle:SetPoint("LEFT", button, "LEFT", 0, 0)
    toggle:EnableMouse(false)

    local label = UI.CreateLabel(button, labelText, 12)
    label:SetPoint("LEFT", toggle, "RIGHT", 8, 0)
    label:SetTextColor(0.92, 0.92, 0.92)
    button.toggle = toggle
    button.label = label

    local function Refresh(animate)
        toggle:SetChecked(getter() == true, animate ~= false and button._wqolToggleInitialized == true)
        button._wqolToggleInitialized = true
    end
    button.Refresh = Refresh

    button:SetScript("OnClick", function()
        local oldValue = getter() == true
        setter(not oldValue)

        local newValue = getter() == true
        if newValue == oldValue then
            Refresh(true)
        else
            toggle:SetChecked(newValue, true)
            button._wqolToggleInitialized = true
        end
    end)
    button:SetScript("OnEnter", function()
        local handler = toggle:GetScript("OnEnter")
        if handler then
            handler(toggle)
        end
    end)
    button:SetScript("OnLeave", function()
        local handler = toggle:GetScript("OnLeave")
        if handler then
            handler(toggle)
        end
    end)
    button:SetScript("OnShow", function()
        Refresh(false)
    end)
    Refresh(false)

    return button
end

function UI.CreateEditBox(parent, getter, setter)
    local editBox = CreateFrame("EditBox", nil, parent, "InputBoxTemplate")
    editBox:SetSize(260, 28)
    editBox:SetAutoFocus(false)
    editBox:SetTextInsets(8, 8, 0, 0)
    editBox:SetText(getter() or "")
    UI.ApplyFont(editBox, 12, "")

    if editBox.Left then editBox.Left:Hide() end
    if editBox.Middle then editBox.Middle:Hide() end
    if editBox.Right then editBox.Right:Hide() end
    UI.SkinFrame(editBox, { light = true })

    editBox:SetScript("OnEnterPressed", function(self)
        setter(self:GetText() or "")
        self:ClearFocus()
    end)
    editBox:SetScript("OnEditFocusLost", function(self)
        setter(self:GetText() or "")
    end)

    return editBox
end

local function GetDropdownRegion(dropdown, key)
    if dropdown and dropdown[key] then
        return dropdown[key]
    end

    local name = dropdown and dropdown.GetName and dropdown:GetName()
    if name then
        return _G[name .. key]
    end

    return nil
end

local function StripDropdownTemplate(dropdown)
    if not dropdown or dropdown._wqolDropdownTemplateStripped then
        return
    end

    for _, key in ipairs({ "Left", "Middle", "Right" }) do
        local region = GetDropdownRegion(dropdown, key)
        if region then
            region:SetTexture(nil)
            region:Hide()
        end
    end

    local button = GetDropdownRegion(dropdown, "Button")
    if button then
        for _, key in ipairs({ "NormalTexture", "PushedTexture", "DisabledTexture", "HighlightTexture" }) do
            local texture = button["Get" .. key] and button["Get" .. key](button)
            if texture then
                texture:SetTexture(nil)
                texture:Hide()
            end
        end

        for _, region in ipairs({ button:GetRegions() }) do
            if region and region.GetObjectType and region:GetObjectType() == "Texture" then
                region:SetTexture(nil)
                region:Hide()
            end
        end
    end

    dropdown._wqolDropdownTemplateStripped = true
end

local function UpdateThemedDropdownStyle(dropdown)
    if not dropdown or not dropdown._wqolBorderEdges then
        return
    end

    local isHovered = dropdown:IsMouseOver()
        or (dropdown._wqolButton and dropdown._wqolButton:IsMouseOver())
        or (dropdown._wqolClickOverlay and dropdown._wqolClickOverlay:IsMouseOver())
    local borderR, borderG, borderB, borderA

    if isHovered then
        borderR, borderG, borderB, borderA = 1, 1, 1, 0.18
        if dropdown._wqolBackground then
            dropdown._wqolBackground:SetVertexColor(0.05, 0.05, 0.05, 0.90)
        end
    else
        borderR, borderG, borderB, borderA = 1, 1, 1, 0.08
        if dropdown._wqolBackground then
            dropdown._wqolBackground:SetVertexColor(0.03, 0.03, 0.03, 0.86)
        end
    end

    for _, edge in pairs(dropdown._wqolBorderEdges) do
        edge:SetVertexColor(borderR, borderG, borderB, borderA)
    end
end

local function ApplyThemeToDropdown(dropdown)
    if not dropdown or dropdown._wqolDropdownStyled then
        return
    end

    StripDropdownTemplate(dropdown)

    local button = GetDropdownRegion(dropdown, "Button")
    local text = GetDropdownRegion(dropdown, "Text")

    local background = dropdown:CreateTexture(nil, "BACKGROUND", nil, -8)
    background:SetPoint("TOPLEFT", dropdown, "TOPLEFT", 20, -2)
    background:SetPoint("BOTTOMRIGHT", dropdown, "BOTTOMRIGHT", -10, 2)
    background:SetTexture("Interface\\Buttons\\WHITE8X8")
    dropdown._wqolBackground = background

    local borderTop = CreateEdge(dropdown, "TOPLEFT", background, "TOPLEFT", -1, 1, "TOPRIGHT", background, "TOPRIGHT", 1, 1, 1)
    local borderBottom = CreateEdge(dropdown, "BOTTOMLEFT", background, "BOTTOMLEFT", -1, -1, "BOTTOMRIGHT", background, "BOTTOMRIGHT", 1, -1, 1)
    local borderLeft = CreateEdge(dropdown, "TOPLEFT", background, "TOPLEFT", -1, 1, "BOTTOMLEFT", background, "BOTTOMLEFT", -1, -1, 1)
    local borderRight = CreateEdge(dropdown, "TOPRIGHT", background, "TOPRIGHT", 1, 1, "BOTTOMRIGHT", background, "BOTTOMRIGHT", 1, -1, 1)

    dropdown._wqolBorderEdges = {
        top = borderTop,
        bottom = borderBottom,
        left = borderLeft,
        right = borderRight,
    }

    if text then
        text:ClearAllPoints()
        text:SetPoint("LEFT", background, "LEFT", 8, 0)
        text:SetPoint("RIGHT", background, "RIGHT", -28, 0)
        text:SetJustifyH("LEFT")
        text:SetTextColor(1, 1, 1, 0.95)
        UI.ApplyFont(text, 12, "")
    end

    local arrow = UI.CreateLabel(dropdown, "v", 12)
    arrow:SetPoint("RIGHT", background, "RIGHT", -8, 0)
    UI.SetAddonColor(arrow, "SetTextColor")
    dropdown.arrow = arrow

    if button then
        button:ClearAllPoints()
        button:SetPoint("TOPLEFT", background, "TOPLEFT", 0, 0)
        button:SetPoint("BOTTOMRIGHT", background, "BOTTOMRIGHT", 0, 0)
        button:SetFrameLevel(dropdown:GetFrameLevel())
        button:EnableMouse(false)
        dropdown._wqolButton = button
    end

    local overlay = CreateFrame("Button", nil, dropdown)
    local openMonitor = CreateFrame("Frame", nil, dropdown)
    openMonitor:Hide()
    openMonitor:SetScript("OnUpdate", function(self)
        local isOpen = UIDROPDOWNMENU_OPEN_MENU == dropdown and DropDownList1 and DropDownList1:IsShown()
        arrow:SetText(isOpen and "^" or "v")
        if not isOpen then
            self:Hide()
        end
    end)

    overlay:SetFrameStrata(dropdown:GetFrameStrata())
    overlay:SetFrameLevel(((button and button:GetFrameLevel()) or dropdown:GetFrameLevel()) + 5)
    overlay:SetPoint("TOPLEFT", background, "TOPLEFT", 0, 0)
    overlay:SetPoint("BOTTOMRIGHT", background, "BOTTOMRIGHT", 0, 0)
    overlay:SetScript("OnClick", function()
        if UIDROPDOWNMENU_OPEN_MENU == dropdown then
            CloseDropDownMenus()
            arrow:SetText("v")
            openMonitor:Hide()
            return
        end

        ToggleDropDownMenu(1, nil, dropdown, dropdown, 20, 0)
        arrow:SetText("^")
        openMonitor:Show()
        ApplyThemeToDropdownMenu(1)
        if C_Timer then
            C_Timer.After(0, function()
                ApplyThemeToDropdownMenu(1)
            end)
        end
    end)
    overlay:SetScript("OnEnter", function()
        UpdateThemedDropdownStyle(dropdown)
    end)
    overlay:SetScript("OnLeave", function()
        UpdateThemedDropdownStyle(dropdown)
    end)
    dropdown._wqolClickOverlay = overlay

    dropdown:HookScript("OnEnter", UpdateThemedDropdownStyle)
    dropdown:HookScript("OnLeave", UpdateThemedDropdownStyle)
    dropdown:HookScript("OnShow", UpdateThemedDropdownStyle)

    dropdown._wqolDropdownStyled = true
    UpdateThemedDropdownStyle(dropdown)
end

function UI.CreateDropdown(parent, width, options, getter, setter)
    local dropdown = CreateFrame("Frame", nil, parent, "UIDropDownMenuTemplate")
    dropdown:SetSize((width or 160) + 30, 30)

    local function GetOptions()
        if type(options) == "function" then
            return options() or {}
        end

        return options or {}
    end

    local function RefreshText()
        local selectedValue = getter()
        UIDropDownMenu_SetText(dropdown, "")
        for _, option in ipairs(GetOptions()) do
            if selectedValue == option.value then
                UIDropDownMenu_SetText(dropdown, TRANSLATE(option.label))
                break
            end
        end
    end

    UIDropDownMenu_SetWidth(dropdown, width or 160)
    ApplyThemeToDropdown(dropdown)
    UIDropDownMenu_Initialize(dropdown, function(_, level)
        if level ~= 1 then
            return
        end

        for _, option in ipairs(GetOptions()) do
            local info = UIDropDownMenu_CreateInfo()
            info.text = TRANSLATE(option.label)
            info.notCheckable = true
            info.func = function()
                setter(option.value)
                UIDropDownMenu_SetText(dropdown, TRANSLATE(option.label))
                if dropdown.arrow then
                    dropdown.arrow:SetText("v")
                end
            end
            UIDropDownMenu_AddButton(info, level)
        end
    end)

    dropdown.RefreshText = RefreshText
    dropdown:HookScript("OnShow", RefreshText)
    RefreshText()

    ApplyThemeToDropdown(dropdown)
    return dropdown
end

local MULTI_DROPDOWN_MAX_ROWS = 8
local MULTI_DROPDOWN_ROW_HEIGHT = 26
local MULTI_DROPDOWN_EMPTY_HEIGHT = 42
local MULTI_DROPDOWN_DELETE_DURATION = 0.14

function UI.CreateMultiSelectDropdown(parent, width, optionsGetter, checkedGetter, setter, textGetter, config)
    config = type(config) == "table" and config or {}
    local onDelete = type(config.onDelete) == "function" and config.onDelete or nil
    local emptyText = tostring(config.emptyText or "No known characters")
    local menuWidth = tonumber(config.menuWidth) or width or 220
    local deleteEnabled = onDelete ~= nil
    local dropdown = CreateFrame("Frame", nil, parent)
    dropdown:SetSize(width or 220, 26)
    dropdown.offset = 0
    dropdown.rows = {}

    local background = dropdown:CreateTexture(nil, "BACKGROUND", nil, -8)
    background:SetAllPoints(dropdown)
    background:SetTexture("Interface\\Buttons\\WHITE8X8")
    background:SetVertexColor(0.03, 0.03, 0.03, 0.86)
    dropdown._wqolBackground = background

    dropdown._wqolBorderEdges = {
        top = CreateEdge(dropdown, "TOPLEFT", dropdown, "TOPLEFT", -1, 1, "TOPRIGHT", dropdown, "TOPRIGHT", 1, 1, 1),
        bottom = CreateEdge(dropdown, "BOTTOMLEFT", dropdown, "BOTTOMLEFT", -1, -1, "BOTTOMRIGHT", dropdown, "BOTTOMRIGHT", 1, -1, 1),
        left = CreateEdge(dropdown, "TOPLEFT", dropdown, "TOPLEFT", -1, 1, "BOTTOMLEFT", dropdown, "BOTTOMLEFT", -1, -1, 1),
        right = CreateEdge(dropdown, "TOPRIGHT", dropdown, "TOPRIGHT", 1, 1, "BOTTOMRIGHT", dropdown, "BOTTOMRIGHT", 1, -1, 1),
    }

    local text = UI.CreateLabel(dropdown, "", 12)
    text:SetPoint("LEFT", dropdown, "LEFT", 8, 0)
    text:SetPoint("RIGHT", dropdown, "RIGHT", -26, 0)
    text:SetTextColor(1, 1, 1, 0.95)
    text:SetJustifyH("LEFT")
    dropdown.text = text

    local arrow = UI.CreateLabel(dropdown, "v", 12)
    arrow:SetPoint("RIGHT", dropdown, "RIGHT", -8, 0)
    UI.SetAddonColor(arrow, "SetTextColor")
    dropdown.arrow = arrow

    local overlay = CreateFrame("Button", nil, dropdown)
    overlay:SetAllPoints(dropdown)
    dropdown._wqolClickOverlay = overlay

    local menu = CreateFrame("Frame", nil, UIParent, "BackdropTemplate")
    menu:SetFrameStrata("DIALOG")
    menu:SetFrameLevel(930)
    menu:EnableMouse(true)
    menu:EnableMouseWheel(true)
    menu:Hide()
    menu.rows = dropdown.rows
    menu.emptyText = menu:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
    menu.emptyText:SetPoint("CENTER", menu, "CENTER", 0, 0)
    menu.emptyText:SetTextColor(0.82, 0.82, 0.82)
    UI.SkinFrame(menu, { light = true })

    menu.scrollbar = CreateFrame("Button", nil, menu)
    menu.scrollbar:SetWidth(2)
    menu.scrollbar:SetPoint("TOPRIGHT", menu, "TOPRIGHT", -5, -6)
    menu.scrollbar:SetPoint("BOTTOMRIGHT", menu, "BOTTOMRIGHT", -5, 6)
    menu.scrollbar:EnableMouse(true)
    menu.scrollbar.track = menu.scrollbar:CreateTexture(nil, "BACKGROUND")
    menu.scrollbar.track:SetAllPoints(menu.scrollbar)
    menu.scrollbar.track:SetTexture("Interface\\Buttons\\WHITE8X8")
    menu.scrollbar.track:SetVertexColor(1, 1, 1, 0.08)
    menu.scrollbar.thumb = CreateFrame("Button", nil, menu.scrollbar)
    menu.scrollbar.thumb:SetWidth(5)
    menu.scrollbar.thumb:SetPoint("CENTER", menu.scrollbar, "CENTER", 0, 0)
    menu.scrollbar.thumb:EnableMouse(true)
    menu.scrollbar.thumb.texture = menu.scrollbar.thumb:CreateTexture(nil, "ARTWORK")
    menu.scrollbar.thumb.texture:SetAllPoints(menu.scrollbar.thumb)
    menu.scrollbar.thumb.texture:SetTexture("Interface\\Buttons\\WHITE8X8")
    menu.scrollbar:Hide()
    dropdown.menu = menu

    local clickMonitor = CreateFrame("Frame")
    clickMonitor:Hide()
    clickMonitor.leftWasDown = false
    clickMonitor.rightWasDown = false

    local function RefreshText()
        text:SetText(textGetter and textGetter() or "")
    end

    local function GetOptions()
        return (optionsGetter and optionsGetter()) or {}
    end

    local function SetHover(row, hovered)
        if row.isHeader then
            row.bg:SetColorTexture(0, 0, 0, 0)
            return
        end

        if hovered then
            local r, g, b = UI.GetClassColor()
            row.bg:SetColorTexture(r * 0.22, g * 0.22, b * 0.22, 0.72)
        else
            row.bg:SetColorTexture(0, 0, 0, 0.34)
        end
    end

    local function SetChecked(row, checked)
        StopCheckBoxAnimation(row)
        row.check:SetShown(checked == true)
        if row.hoverPreview then
            row.hoverPreview:SetShown(row:IsMouseOver() and checked ~= true)
        end
    end

    local function SetRowTextColor(row, classFile)
        local colors = CUSTOM_CLASS_COLORS or RAID_CLASS_COLORS
        local color = colors and classFile and colors[classFile]
        if color then
            row.label:SetTextColor(color.r or 1, color.g or 1, color.b or 1, 1)
        else
            row.label:SetTextColor(1, 1, 1, 1)
        end
    end

    local RefreshList

    local function GetDeleteProgress(option)
        local animation = menu.deleteAnimation
        if not animation or not option or animation.value ~= option.value then
            return 0
        end

        return math.max(0, math.min((tonumber(animation.elapsed) or 0) / MULTI_DROPDOWN_DELETE_DURATION, 1))
    end

    local function GetOptionHeight(option)
        if not option then
            return 0
        end

        if option.isHeader then
            return MULTI_DROPDOWN_ROW_HEIGHT
        end

        return MULTI_DROPDOWN_ROW_HEIGHT * (1 - GetDeleteProgress(option))
    end

    local function FinishDeleteAnimation()
        local animation = menu.deleteAnimation
        if not animation then
            return
        end

        local value = animation.value
        menu.deleteAnimation = nil
        menu:SetScript("OnUpdate", nil)
        onDelete(value)
        RefreshText()
        if RefreshList then
            RefreshList()
        end
    end

    local function UpdateDeleteAnimation(self, delta)
        if not self.deleteAnimation then
            self:SetScript("OnUpdate", nil)
            return
        end

        self.deleteAnimation.elapsed = (tonumber(self.deleteAnimation.elapsed) or 0) + (delta or 0)
        if self.deleteAnimation.elapsed >= MULTI_DROPDOWN_DELETE_DURATION then
            FinishDeleteAnimation()
            return
        end

        if RefreshList then
            RefreshList()
        end
    end

    local function StartDeleteAnimation(value)
        if not deleteEnabled or value == nil then
            return
        end

        if menu.deleteAnimation and menu.deleteAnimation.value == value then
            return
        end

        menu.deleteAnimation = {
            value = value,
            elapsed = 0,
        }
        dropdown.offset = 0
        menu:SetScript("OnUpdate", UpdateDeleteAnimation)
        if RefreshList then
            RefreshList()
        end
    end

    local function CreateRow(index)
        local row = CreateFrame("Button", nil, menu)
        row:SetFrameLevel((menu:GetFrameLevel() or 930) + 1)
        row:SetHeight(MULTI_DROPDOWN_ROW_HEIGHT - 2)
        row.bg = row:CreateTexture(nil, "BACKGROUND")
        row.bg:SetAllPoints(row)
        row.bg:SetColorTexture(0, 0, 0, 0.34)

        row.box = CreateFrame("Frame", nil, row, "BackdropTemplate")
        row.box:SetPoint("LEFT", row, "LEFT", 8, 0)
        row.box:SetSize(14, 14)
        UI.SkinFrame(row.box, { light = true })

        row.check = row.box:CreateTexture(nil, "OVERLAY")
        row.check:SetPoint("CENTER")
        row.check:SetSize(CHECKBOX_CHECK_SIZE, CHECKBOX_CHECK_SIZE)
        row.check:SetTexture("Interface\\Buttons\\WHITE8X8")
        UI.SetAddonColor(row.check, "SetVertexColor")
        row._wqolCheckTexture = row.check

        row.hoverPreview = row.box:CreateTexture(nil, "OVERLAY")
        row.hoverPreview:SetPoint("CENTER")
        row.hoverPreview:SetSize(CHECKBOX_HOVER_SIZE, CHECKBOX_HOVER_SIZE)
        row.hoverPreview:SetTexture("Interface\\Buttons\\WHITE8X8")
        UI.SetAddonColor(row.hoverPreview, "SetVertexColor")
        row.hoverPreview:SetAlpha(0.55)
        row.hoverPreview:Hide()
        row._wqolHoverPreview = row.hoverPreview

        row.label = row:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
        row.label:SetPoint("LEFT", row.box, "RIGHT", 8, 0)
        row.label:SetPoint("RIGHT", row, "RIGHT", -8, 0)
        row.label:SetJustifyH("LEFT")
        row.label:SetTextColor(1, 1, 1, 1)
        UI.ApplyFont(row.label, 12, "")

        row.header = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
        row.header:SetPoint("LEFT", row, "LEFT", 8, 0)
        row.header:SetPoint("RIGHT", row, "RIGHT", -8, 0)
        row.header:SetJustifyH("LEFT")
        row.header:SetTextColor(1, 0.82, 0, 1)
        UI.ApplyFont(row.header, 12, "")
        row.header:Hide()

        if deleteEnabled then
            row.delete = CreateFrame("Button", nil, row)
            row.delete:SetFrameLevel((row:GetFrameLevel() or 0) + 2)
            row.delete:SetSize(24, 24)
            row.delete:SetPoint("RIGHT", row, "RIGHT", -2, 0)
            row.delete.text = row.delete:CreateFontString(nil, "OVERLAY", "GameFontNormal")
            row.delete.text:SetPoint("CENTER", row.delete, "CENTER", 0, 0)
            row.delete.text:SetText("x")
            row.delete.text:SetTextColor(0.9, 0.25, 0.25, 1)
            UI.ApplyFont(row.delete.text, 12, "")
            row.delete:SetScript("OnClick", function(self)
                local option = self:GetParent().option
                if option and not option.isHeader then
                    StartDeleteAnimation(option.value)
                end
            end)
            row.delete:SetScript("OnEnter", function(self)
                SetHover(self:GetParent(), true)
                self.text:SetTextColor(1, 0.36, 0.36, 1)
            end)
            row.delete:SetScript("OnLeave", function(self)
                SetHover(self:GetParent(), false)
                self.text:SetTextColor(0.9, 0.25, 0.25, 1)
            end)
            row.delete:Hide()
        end

        row:SetScript("OnEnter", function(self)
            SetHover(self, true)
            if not self.isHeader and self.option and checkedGetter and checkedGetter(self.option.value) ~= true and not self._wqolCheckAnimation then
                self.hoverPreview:Show()
            end
        end)
        row:SetScript("OnLeave", function(self)
            SetHover(self, false)
            self.hoverPreview:Hide()
        end)
        row:SetScript("OnClick", function(self)
            if not self.option or self.isHeader then
                return
            end

            local oldValue = checkedGetter and checkedGetter(self.option.value) == true
            setter(self.option.value, not oldValue)
            RefreshText()
            local newValue = checkedGetter and checkedGetter(self.option.value) == true
            self.hoverPreview:Hide()
            if newValue == oldValue then
                SetChecked(self, newValue)
            elseif newValue then
                StartCheckBoxCheckAnimation(self)
            else
                StartCheckBoxUncheckAnimation(self)
            end
        end)

        dropdown.rows[index] = row
        return row
    end

    local function RefreshScrollbar(options, menuHeight, maxOffset)
        menu.scrollbar:SetShown(#options > MULTI_DROPDOWN_MAX_ROWS)
        if #options <= MULTI_DROPDOWN_MAX_ROWS then
            menu.scrollbar.thumb:Hide()
            return
        end

        local r, g, b = UI.GetClassColor()
        local trackHeight = math.max(menuHeight - 12, 1)
        local thumbHeight = math.max(18, trackHeight * (MULTI_DROPDOWN_MAX_ROWS / #options))
        local travel = math.max(trackHeight - thumbHeight, 0)
        local offsetRatio = maxOffset > 0 and ((dropdown.offset or 0) / maxOffset) or 0

        menu.scrollbar.thumb:ClearAllPoints()
        menu.scrollbar.thumb:SetPoint("TOP", menu.scrollbar, "TOP", 0, -(travel * offsetRatio))
        menu.scrollbar.thumb:SetHeight(thumbHeight)
        menu.scrollbar.thumb.texture:SetVertexColor(r, g, b, 0.9)
        menu.scrollbar.thumb:Show()
    end

    local function SetScrollOffsetFromCursor()
        local maxOffset = tonumber(dropdown.maxOffset) or 0
        if maxOffset <= 0 then
            dropdown.offset = 0
            return
        end

        local scale = menu.scrollbar:GetEffectiveScale() or 1
        local cursorY = select(2, GetCursorPosition())
        local scrollbarTop = menu.scrollbar:GetTop()
        local trackHeight = menu.scrollbar:GetHeight() or 1
        local thumbHeight = menu.scrollbar.thumb:GetHeight() or 1
        local travel = math.max(trackHeight - thumbHeight, 1)
        local relativeY = ((scrollbarTop or 0) * scale - (cursorY or 0)) / scale
        local offsetRatio = math.max(0, math.min((relativeY - (thumbHeight / 2)) / travel, 1))

        dropdown.offset = math.max(0, math.min(math.floor((offsetRatio * maxOffset) + 0.5), maxOffset))
    end

    RefreshList = function()
        local options = GetOptions()
        local maxOffset = math.max(0, #options - MULTI_DROPDOWN_MAX_ROWS)
        dropdown.maxOffset = maxOffset
        dropdown.offset = math.max(0, math.min(tonumber(dropdown.offset) or 0, maxOffset))

        local rowCount = math.min(#options, MULTI_DROPDOWN_MAX_ROWS)
        local menuHeight = MULTI_DROPDOWN_EMPTY_HEIGHT
        if rowCount > 0 then
            menuHeight = 10
            for index = 1, rowCount do
                menuHeight = menuHeight + GetOptionHeight(options[(dropdown.offset or 0) + index])
            end
        end
        menu:SetSize(menuWidth, menuHeight)
        menu:ClearAllPoints()
        menu:SetPoint("TOPLEFT", dropdown, "BOTTOMLEFT", 0, -4)
        menu.emptyText:SetText(TRANSLATE(emptyText))
        menu.emptyText:SetShown(rowCount == 0)
        RefreshScrollbar(options, menuHeight, maxOffset)

        local rowTop = 6
        for index = 1, MULTI_DROPDOWN_MAX_ROWS do
            local row = dropdown.rows[index]
            local option = options[(dropdown.offset or 0) + index]

            if option then
                row = row or CreateRow(index)
                row.option = option
                row.isHeader = option.isHeader == true
                row:ClearAllPoints()
                row:SetPoint("TOPLEFT", menu, "TOPLEFT", 6, -rowTop)
                row:SetPoint("RIGHT", menu, "RIGHT", (#options > MULTI_DROPDOWN_MAX_ROWS) and -12 or -6, 0)
                row:SetHeight(math.max(GetOptionHeight(option) - 2, 1))
                row:SetAlpha(1 - GetDeleteProgress(option))
                if row.isHeader then
                    row.bg:SetColorTexture(0, 0, 0, 0)
                    row.box:Hide()
                    row.check:Hide()
                    row.hoverPreview:Hide()
                    StopCheckBoxAnimation(row)
                    row.label:Hide()
                    if row.delete then
                        row.delete:Hide()
                    end
                    row.header:SetText(TRANSLATE(option.label))
                    row.header:Show()
                else
                    row.box:Show()
                    row.label:Show()
                    row.header:Hide()
                    row.label:ClearAllPoints()
                    row.label:SetPoint("LEFT", row.box, "RIGHT", 8, 0)
                    if row.delete then
                        row.label:SetPoint("RIGHT", row.delete, "LEFT", -4, 0)
                        row.delete:Show()
                    else
                        row.label:SetPoint("RIGHT", row, "RIGHT", -8, 0)
                    end
                    row.label:SetText(TRANSLATE(option.label))
                    SetRowTextColor(row, option.classFile)
                    UI.SetAddonColor(row.check, "SetVertexColor")
                    UI.SetAddonColor(row.hoverPreview, "SetVertexColor")
                    SetChecked(row, checkedGetter and checkedGetter(option.value) == true)
                    SetHover(row, false)
                end
                rowTop = rowTop + GetOptionHeight(option)
                row:Show()
            elseif row then
                row.option = nil
                row.isHeader = false
                row:Hide()
            end
        end
    end

    dropdown.RefreshList = RefreshList

    local function Open()
        RefreshList()
        menu:Show()
        arrow:SetText("^")
        clickMonitor:Show()
    end

    local function Close()
        menu:Hide()
        arrow:SetText("v")
        clickMonitor:Hide()
    end

    function dropdown.Close()
        Close()
    end

    dropdown:HookScript("OnHide", Close)

    local function Toggle()
        if menu:IsShown() then
            Close()
        else
            Open()
        end
    end

    local function RefreshHover()
        local hovered = dropdown:IsMouseOver() or overlay:IsMouseOver()
        local alpha = hovered and 0.18 or 0.08
        background:SetVertexColor(hovered and 0.05 or 0.03, hovered and 0.05 or 0.03, hovered and 0.05 or 0.03, hovered and 0.90 or 0.86)
        for _, edge in pairs(dropdown._wqolBorderEdges) do
            edge:SetVertexColor(1, 1, 1, alpha)
        end
    end

    overlay:SetScript("OnClick", Toggle)
    overlay:SetScript("OnEnter", RefreshHover)
    overlay:SetScript("OnLeave", RefreshHover)
    dropdown:HookScript("OnShow", RefreshText)
    dropdown:HookScript("OnEnter", RefreshHover)
    dropdown:HookScript("OnLeave", RefreshHover)

    menu:SetScript("OnMouseWheel", function(_, delta)
        local maxOffset = tonumber(dropdown.maxOffset) or 0
        if maxOffset <= 0 then
            return
        end

        dropdown.offset = math.max(0, math.min((dropdown.offset or 0) - (delta > 0 and 1 or -1), maxOffset))
        RefreshList()
    end)

    menu.scrollbar:SetScript("OnMouseDown", function()
        if not menu.scrollbar:IsShown() then
            return
        end

        SetScrollOffsetFromCursor()
        RefreshList()
    end)

    menu.scrollbar.thumb:SetScript("OnMouseDown", function(self)
        self.dragging = true
        menu.scrollbar.dragging = true
        SetScrollOffsetFromCursor()
        RefreshList()
    end)

    menu.scrollbar.thumb:SetScript("OnMouseUp", function(self)
        self.dragging = false
        menu.scrollbar.dragging = false
    end)

    menu.scrollbar.thumb:SetScript("OnHide", function(self)
        self.dragging = false
        menu.scrollbar.dragging = false
    end)

    menu.scrollbar.thumb:SetScript("OnUpdate", function(self)
        if self.dragging and IsMouseButtonDown and IsMouseButtonDown("LeftButton") then
            SetScrollOffsetFromCursor()
            RefreshList()
        elseif self.dragging then
            self.dragging = false
            menu.scrollbar.dragging = false
        end
    end)

    local function IsMouseOverFrame(frame)
        return frame and frame:IsShown() and MouseIsOver and MouseIsOver(frame)
    end

    clickMonitor:SetScript("OnUpdate", function(self)
        local leftDown = IsMouseButtonDown and IsMouseButtonDown("LeftButton")
        local rightDown = IsMouseButtonDown and IsMouseButtonDown("RightButton")
        local clicked = (leftDown and not self.leftWasDown) or (rightDown and not self.rightWasDown)

        self.leftWasDown = leftDown
        self.rightWasDown = rightDown

        if not clicked then
            return
        end

        if IsMouseOverFrame(menu) or IsMouseOverFrame(dropdown) then
            return
        end

        Close()
    end)

    menu:SetScript("OnHide", function()
        if menu.deleteAnimation then
            FinishDeleteAnimation()
        end
        arrow:SetText("v")
        clickMonitor:Hide()
    end)

    dropdown.RefreshText = RefreshText
    RefreshText()
    RefreshHover()

    return dropdown
end

local function CreateNativeTabButton(parent, text)
    local button = CreateFrame("Button", nil, parent)
    button:SetSize(TAB_BUTTON_WIDTH, 24)

    local bg = button:CreateTexture(nil, "BACKGROUND")
    bg:SetAllPoints(button)
    bg:SetTexture("Interface\\Buttons\\WHITE8X8")
    bg:Hide()
    button.wqolBackground = bg

    button.wqolBorder = {
        top = CreateEdge(button, "TOPLEFT", button, "TOPLEFT", 0, 0, "TOPRIGHT", button, "TOPRIGHT", 0, 0, 1),
        bottom = CreateEdge(button, "BOTTOMLEFT", button, "BOTTOMLEFT", 0, 0, "BOTTOMRIGHT", button, "BOTTOMRIGHT", 0, 0, 1),
        left = CreateEdge(button, "TOPLEFT", button, "TOPLEFT", 0, 0, "BOTTOMLEFT", button, "BOTTOMLEFT", 0, 0, 1),
        right = CreateEdge(button, "TOPRIGHT", button, "TOPRIGHT", 0, 0, "BOTTOMRIGHT", button, "BOTTOMRIGHT", 0, 0, 1),
    }

    local label = button:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
    label:SetPoint("CENTER", button, "CENTER", 0, 0)
    label:SetText(text)
    UI.ApplyFont(label, 12, "OUTLINE")
    button.Text = label

    return button
end

local function UpdateNativeTabButton(button, isActive)
    if not button or not button.Text then
        return
    end

    local cr, cg, cb = UI.GetClassColor()
    if isActive then
        button.Text:SetTextColor(cr, cg, cb, 1)
    else
        button.Text:SetTextColor(1, 1, 1, 0.92)
    end

    if button.wqolBackground then
        button.wqolBackground:Hide()
    end

    for _, edge in pairs(button.wqolBorder or {}) do
        edge:Hide()
    end
end

local function PositionTabActiveIndicatorAtButton(indicatorFrame, button)
    if not indicatorFrame or not button then
        return
    end

    indicatorFrame:ClearAllPoints()
    indicatorFrame:SetPoint("BOTTOMLEFT", button, "BOTTOMLEFT", 0, 0)
    indicatorFrame:SetPoint("BOTTOMRIGHT", button, "BOTTOMRIGHT", 0, 0)
    indicatorFrame:SetHeight(2)
end

local function EnsureTabActiveIndicatorFrame(tabsBar)
    if not tabsBar or tabsBar.wqolActiveIndicatorFrame then
        return tabsBar and tabsBar.wqolActiveIndicatorFrame or nil
    end

    local frame = CreateFrame("Frame", nil, tabsBar)
    frame:SetFrameStrata(tabsBar:GetFrameStrata())
    frame:SetFrameLevel(tabsBar:GetFrameLevel() + 20)
    frame:Hide()

    local texture = frame:CreateTexture(nil, "ARTWORK", nil, 7)
    texture:SetAllPoints()
    UI.SetAddonColor(texture, "SetColorTexture", 1, 0.95)

    local animationGroup = frame:CreateAnimationGroup()
    local translation = animationGroup:CreateAnimation("Translation")
    translation:SetDuration(0.16)
    translation:SetSmoothing("OUT")

    animationGroup:SetScript("OnFinished", function()
        translation:SetOffset(0, 0)
        if frame._wqolTargetButton then
            PositionTabActiveIndicatorAtButton(frame, frame._wqolTargetButton)
            frame:Show()
        else
            frame:Hide()
        end
        frame._wqolTargetButton = nil
    end)

    animationGroup:SetScript("OnStop", function()
        translation:SetOffset(0, 0)
        if frame._wqolTargetButton then
            PositionTabActiveIndicatorAtButton(frame, frame._wqolTargetButton)
            frame:Show()
        end
        frame._wqolTargetButton = nil
    end)

    tabsBar.wqolActiveIndicatorFrame = frame
    tabsBar.wqolActiveIndicatorAnimationGroup = animationGroup
    tabsBar.wqolActiveIndicatorTranslation = translation
    return frame
end

local function UpdateTabActiveIndicator(tabsBar, oldButton, newButton)
    local indicatorFrame = EnsureTabActiveIndicatorFrame(tabsBar)
    if not indicatorFrame or not newButton then
        return
    end

    local texture = indicatorFrame:GetRegions()
    if texture then
        UI.SetAddonColor(texture, "SetColorTexture", 1, 0.95)
    end

    local animationGroup = tabsBar.wqolActiveIndicatorAnimationGroup
    local translation = tabsBar.wqolActiveIndicatorTranslation
    if animationGroup and animationGroup:IsPlaying() then
        animationGroup:Stop()
    end

    local shouldAnimate = oldButton and oldButton ~= newButton and oldButton:IsVisible() and newButton:IsVisible()
    if shouldAnimate and animationGroup and translation then
        local oldLeft, oldTop = oldButton:GetLeft(), oldButton:GetTop()
        local newLeft, newTop = newButton:GetLeft(), newButton:GetTop()
        if oldLeft and oldTop and newLeft and newTop then
            PositionTabActiveIndicatorAtButton(indicatorFrame, oldButton)
            indicatorFrame:Show()
            indicatorFrame._wqolTargetButton = newButton
            translation:SetOffset(newLeft - oldLeft, newTop - oldTop)
            animationGroup:Play()
            return
        end
    end

    PositionTabActiveIndicatorAtButton(indicatorFrame, newButton)
    indicatorFrame:Show()
end

local function CreateNativeContentAdapter(parentFrame)
    local adapter = {
        frame = parentFrame,
        children = {},
    }

    function adapter:AddChild(child)
        if not child then
            return
        end

        self.children[#self.children + 1] = child

        local childFrame = child.frame or child
        if childFrame and childFrame.SetParent then
            childFrame:SetParent(self.frame)
            childFrame:ClearAllPoints()
            childFrame:SetPoint("TOPLEFT", self.frame, "TOPLEFT", 0, 0)
            childFrame:SetPoint("BOTTOMRIGHT", self.frame, "BOTTOMRIGHT", 0, 0)
            childFrame:Show()
        end
    end

    function adapter:ReleaseChildren()
        for _, child in ipairs(self.children) do
            local childFrame = child and (child.frame or child)
            if childFrame then
                childFrame:Hide()
                childFrame:SetParent(UIParent)
            end
        end

        self.children = {}
    end

    return adapter
end

function WipedQoL:RegisterTabBuilder(key, builder)
    self.tabBuilders = self.tabBuilders or {}
    self.tabBuilders[key] = builder
end

function WipedQoL:LockConfigurationMovers()
    if self.CloseLootMoverAnchors then
        self:CloseLootMoverAnchors()
    end
end

function WipedQoL:SelectMainTab(key)
    key = key or "general"
    if key == "economy" then
        key = "banking"
    end

    if not self.tabBuilders or not self.tabBuilders[key] then
        key = "general"
    end

    local previousKey = self.activeTab
    if previousKey and previousKey ~= key then
        self:LockConfigurationMovers()
    end
    local previousButton = self.mainWindow and self.mainWindow.tabButtons and self.mainWindow.tabButtons[previousKey]
    self.activeTab = key

    if self.db and self.db.profile and self.db.profile.window then
        self.db.profile.window.activeTab = key
    end

    if self.mainWindow and self.mainWindow.tabButtons then
        for _, tab in ipairs(TAB_ORDER) do
            UpdateNativeTabButton(self.mainWindow.tabButtons[tab.key], tab.key == key)
        end

        local newButton = self.mainWindow.tabButtons[key]
        if self.mainWindow.tabsBar and newButton then
            UpdateTabActiveIndicator(self.mainWindow.tabsBar, previousButton, newButton)
        end
    end

    if self.mainWindow and self.mainWindow.contentAdapter then
        self.mainWindow.contentAdapter:ReleaseChildren()

        local builder = self.tabBuilders and self.tabBuilders[key]
        if builder then
            local view = builder(self, self.mainWindow.contentAdapter.frame)
            self.mainWindow.contentAdapter:AddChild(view)
        end
    end

    if self.RefreshWeeklyTaskReminder then
        self:RefreshWeeklyTaskReminder()
    end
end

function WipedQoL:CreateMainWindow()
    local profile = GetProfile()
    local windowConfig = profile.window
    windowConfig.width = math.max(tonumber(windowConfig.width) or 700, 700)
    windowConfig.height = math.max(tonumber(windowConfig.height) or 430, 430)

    local frame = CreateFrame("Frame", "WipedQoLMainWindowFrame", UIParent, "BackdropTemplate")
    frame:SetSize(windowConfig.width, windowConfig.height)
    frame:SetPoint(windowConfig.point or "CENTER", UIParent, windowConfig.relativePoint or "CENTER", windowConfig.x or 0, windowConfig.y or 0)
    frame:SetFrameStrata("DIALOG")
    frame:SetMovable(true)
    frame:EnableMouse(true)
    frame:RegisterForDrag("LeftButton")
    frame:SetClampedToScreen(true)
    frame:Hide()
    UI.SkinFrame(frame)
    frame:SetScript("OnHide", function()
        if self.CloseDatapanelMenus then
            self:CloseDatapanelMenus()
        end
        self:LockConfigurationMovers()
        if self.RefreshWeeklyTaskReminder then
            self:RefreshWeeklyTaskReminder()
        end
    end)

    if UISpecialFrames then
        table.insert(UISpecialFrames, "WipedQoLMainWindowFrame")
    end

    frame:SetScript("OnDragStart", frame.StartMoving)
    frame:SetScript("OnDragStop", function(self)
        self:StopMovingOrSizing()
        local point, _, relativePoint, x, y = self:GetPoint(1)
        windowConfig.point = point
        windowConfig.relativePoint = relativePoint
        windowConfig.x = x
        windowConfig.y = y
    end)

    local titleWiped = UI.CreateLabel(frame, "Wiped", 16)
    titleWiped:SetTextColor(1, 1, 1)

    local titleQoL = UI.CreateLabel(frame, "QoL", 16)
    UI.SetAddonColor(titleQoL, "SetTextColor")
    frame.titleQoL = titleQoL

    local titleWidth = titleWiped:GetStringWidth() + titleQoL:GetStringWidth()
    titleWiped:SetPoint("TOPLEFT", frame, "TOP", -(titleWidth / 2), -16)
    titleQoL:SetPoint("LEFT", titleWiped, "RIGHT", 0, 0)

    local debugReloadButton = CreateFrame("Button", nil, frame, "UIPanelButtonTemplate")
    debugReloadButton:SetSize(68, 22)
    debugReloadButton:SetPoint("TOPLEFT", frame, "TOPLEFT", 8, -8)
    debugReloadButton:SetText("/reload")
    debugReloadButton:SetScript("OnClick", ReloadUI)
    debugReloadButton:SetShown(self:IsDebugEnabled())
    UI.SkinButton(debugReloadButton)
    frame.debugReloadButton = debugReloadButton

    local closeButton = CreateFrame("Button", nil, frame, "UIPanelCloseButton")
    closeButton:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -8, -8)
    closeButton:SetScript("OnClick", function()
        frame:Hide()
    end)
    UI.SkinCloseButton(closeButton)

    local tabsBar = CreateFrame("Frame", nil, frame)
    tabsBar:SetPoint("TOPLEFT", frame, "TOPLEFT", 18, -48)
    tabsBar:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -18, -48)
    tabsBar:SetHeight(24)

    local contentFrame = CreateFrame("Frame", nil, frame)
    contentFrame:SetPoint("TOPLEFT", frame, "TOPLEFT", 18, -84)
    contentFrame:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -18, 18)

    local contentAdapter = CreateNativeContentAdapter(contentFrame)
    local tabButtons = {}
    local previousButton
    for index, tab in ipairs(TAB_ORDER) do
        local button = CreateNativeTabButton(tabsBar, TRANSLATE(tab.label))
        tabButtons[tab.key] = button

        if index == 1 then
            button:SetPoint("TOPLEFT", tabsBar, "TOPLEFT", 0, 0)
        else
            button:SetPoint("LEFT", previousButton, "RIGHT", TAB_BUTTON_SPACING, 0)
        end

        button:SetScript("OnClick", function()
            self:SelectMainTab(tab.key)
        end)
        previousButton = button
    end

    frame.contentAdapter = contentAdapter
    frame.tabButtons = tabButtons
    frame.tabsBar = tabsBar
    self.mainWindow = frame

    local activeTab = windowConfig.activeTab or self.activeTab or "general"
    if activeTab == "economy" then
        activeTab = "banking"
        windowConfig.activeTab = activeTab
    end

    if not self.tabBuilders or not self.tabBuilders[activeTab] then
        activeTab = "general"
        windowConfig.activeTab = activeTab
    end
    self:SelectMainTab(activeTab)

    return frame
end

function WipedQoL:ShowMainWindow()
    if self:IsInCombat() then
        return
    end

    local frame = self.mainWindow or self:CreateMainWindow()
    frame:Show()
    if self.RefreshWeeklyTaskReminder then
        self:RefreshWeeklyTaskReminder()
    end
end

function WipedQoL:HideMainWindow()
    if self.mainWindow then
        self.mainWindow:Hide()
    end
end

function WipedQoL:IsMainWindowShown()
    return self.mainWindow and self.mainWindow:IsShown()
end

function WipedQoL:ToggleMainWindow()
    if self:IsMainWindowShown() then
        self:HideMainWindow()
    else
        self:ShowMainWindow()
    end
end

function WipedQoL:RefreshLocale()
    local wasShown = self:IsMainWindowShown()

    if self.mainWindow then
        self.mainWindow:Hide()
        self.mainWindow = nil
    end

    if wasShown then
        self:ShowMainWindow()
    end
end
