local ADDON_NAME = ...
NaijsMPlusTimerDB = NaijsMPlusTimerDB or {}

local DB_DEFAULTS = {
    point = "RIGHT",
    relativePoint = "RIGHT",
    x = -24,
    y = 0,
    scale = 1.00,
    alpha = 0.50,

    borderR = 0.2118,
    borderG = 0.9216,
    borderB = 1.0000,

    barR = 0.5490,
    barG = 0.8510,
    barB = 1.0000,

    deathSound = true,
    forcesSound = true,
    depletedSound = true,
    deathVolume = 0.50,
    forcesVolume = 0.50,
    depletedVolume = 0.50,
    locked = false,
}

local MIN_PANEL_HEIGHT = 124
local OBJECTIVE_ROW_HEIGHT = 15
local OBJECTIVE_TOP_Y = -122

local lastDeaths = nil
local playedForces100 = false
local playedDepletedSound = false
local lastActiveRunInfo = nil
local finishedRunInfo = nil
local showFinishedRun = false

local function ApplyDefaults()
    for k, v in pairs(DB_DEFAULTS) do
        if NaijsMPlusTimerDB[k] == nil then
            NaijsMPlusTimerDB[k] = v
        end
    end

    if math.abs((NaijsMPlusTimerDB.borderR or 0) - 0.52) < 0.001
        and math.abs((NaijsMPlusTimerDB.borderG or 0) - 0.60) < 0.001
        and math.abs((NaijsMPlusTimerDB.borderB or 0) - 0.74) < 0.001 then
        NaijsMPlusTimerDB.borderR = DB_DEFAULTS.borderR
        NaijsMPlusTimerDB.borderG = DB_DEFAULTS.borderG
        NaijsMPlusTimerDB.borderB = DB_DEFAULTS.borderB
    end

    if math.abs((NaijsMPlusTimerDB.barR or 0) - 0.55) < 0.001
        and math.abs((NaijsMPlusTimerDB.barG or 0) - 0.85) < 0.001
        and math.abs((NaijsMPlusTimerDB.barB or 0) - 1.00) < 0.001 then
        NaijsMPlusTimerDB.barR = DB_DEFAULTS.barR
        NaijsMPlusTimerDB.barG = DB_DEFAULTS.barG
        NaijsMPlusTimerDB.barB = DB_DEFAULTS.barB
    end
end

local function SafeCall(fn, ...)
    local ok, a, b, c, d, e, f, g, h, i, j, k, l, m = pcall(fn, ...)
    if ok then
        return a, b, c, d, e, f, g, h, i, j, k, l, m
    end
end

local function IsMPlusActive()
    return SafeCall(function()
        return C_ChallengeMode
            and C_ChallengeMode.IsChallengeModeActive
            and C_ChallengeMode.IsChallengeModeActive()
    end)
end

local function HasScenarioObjectives()
    local count = SafeCall(function()
        if C_ScenarioInfo and C_ScenarioInfo.GetStepInfo then
            local stepInfo = C_ScenarioInfo.GetStepInfo()
            if type(stepInfo) == "table" then
                return stepInfo.numCriteria or stepInfo.criteriaCount or 0
            end
        end
    end)

    if count and count > 0 then
        return true
    end

    count = SafeCall(function()
        if C_Scenario and C_Scenario.GetStepInfo then
            local _, _, numCriteria = C_Scenario.GetStepInfo()
            return numCriteria
        end
    end)

    return count and count > 0
end

local function IsPartyInstance()
    local inInstance, instanceType = SafeCall(function()
        return IsInInstance()
    end)

    return inInstance and instanceType == "party"
end

local function IsKeystoneInstance()
    if not IsPartyInstance() then
        return false
    end

    local _, _, difficultyID = SafeCall(function()
        return GetInstanceInfo()
    end)

    if difficultyID == 8 then
        return true
    end

    return false
end

local function ShouldUseNaijsTracker()
    if IsMPlusActive() then
        return true
    end

    if IsKeystoneInstance() then
        return true
    end

    if IsPartyInstance() and HasScenarioObjectives() then
        return true
    end

    return false
end

local function FormatClock(seconds)
    seconds = math.floor(seconds or 0)
    local negative = seconds < 0
    seconds = math.abs(seconds)
    local m = math.floor(seconds / 60)
    local s = seconds % 60

    if negative then
        return string.format("-%02d:%02d", m, s)
    end

    return string.format("%02d:%02d", m, s)
end

local function StatusColor(status)
    if status == "+++" then return 0.35, 1.00, 0.55 end
    if status == "++" then return 0.55, 0.85, 1.00 end
    if status == "+" then return 1.00, 0.85, 0.35 end
    if status == "DEPLETED" then return 1.00, 0.25, 0.25 end
    return 0.75, 0.75, 0.75
end

local function UpgradeStatus(remaining, limit)
    if not limit or limit <= 0 then
        return "-", 0
    end

    local pct = remaining / limit

    if remaining <= 0 then
        return "DEPLETED", pct
    elseif pct > 0.40 then
        return "+++", pct
    elseif pct > 0.20 then
        return "++", pct
    else
        return "+", pct
    end
end

local function Clamp(value, minValue, maxValue)
    value = tonumber(value) or minValue
    if value < minValue then return minValue end
    if value > maxValue then return maxValue end
    return value
end

local function GetVolumeStep(volume)
    volume = Clamp(volume or 0.50, 0.00, 1.00)
    if volume <= 0 then
        return 0
    end

    local step = math.floor(((volume * 100) / 5) + 0.5) * 5
    return math.max(5, math.min(100, step))
end

local function PlayBundledVolumeSound(stem, volume)
    local step = GetVolumeStep(volume)
    if step <= 0 then
        return
    end

    local path = string.format("Interface\\AddOns\\NaijsMPlusTimer\\Sounds\\%s_%03d.ogg", stem, step)
    SafeCall(function()
        PlaySoundFile(path, "Master")
    end)
end

local function PlayConfiguredSound(kind)
    if kind == "death" then
        if not NaijsMPlusTimerDB.deathSound then return end
        PlayBundledVolumeSound("death", NaijsMPlusTimerDB.deathVolume or 0.50)
        return
    end

    if kind == "forces" then
        if not NaijsMPlusTimerDB.forcesSound then return end
        PlayBundledVolumeSound("forces", NaijsMPlusTimerDB.forcesVolume or 0.50)
        return
    end

    if kind == "depleted" then
        if not NaijsMPlusTimerDB.depletedSound then return end
        PlayBundledVolumeSound("depleted", NaijsMPlusTimerDB.depletedVolume or 0.50)
        return
    end
end
local frame = CreateFrame("Frame", "NaijsMPlusTimerFrame", UIParent, "BackdropTemplate")
frame:SetSize(324, 200)
frame:SetClampedToScreen(true)
frame:SetMovable(true)
frame:EnableMouse(true)
frame:RegisterForDrag("LeftButton")
frame:SetFrameStrata("LOW")
frame:SetFrameLevel(5)
frame:SetBackdrop({
    bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background-Dark",
    edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
    tile = true,
    tileSize = 16,
    edgeSize = 14,
    insets = { left = 4, right = 4, top = 4, bottom = 4 },
})

frame.headerGlow = frame:CreateTexture(nil, "ARTWORK")
frame.headerGlow:SetColorTexture(0.38, 0.48, 0.70, 0.10)
frame.headerGlow:SetPoint("TOPLEFT", frame, 8, -8)
frame.headerGlow:SetPoint("TOPRIGHT", frame, -8, -8)
frame.headerGlow:SetHeight(28)

frame.divider = frame:CreateTexture(nil, "ARTWORK")
frame.divider:SetColorTexture(1, 1, 1, 0.08)
frame.divider:SetPoint("TOPLEFT", frame, 16, -96)
frame.divider:SetPoint("TOPRIGHT", frame, -16, -96)
frame.divider:SetHeight(1)

frame.title = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
frame.title:SetPoint("TOPLEFT", frame, 18, -16)
frame.title:SetText("MYTHIC+")
frame.title:SetTextColor(0.72, 0.77, 0.84)

frame.key = frame:CreateFontString(nil, "OVERLAY", "GameFontHighlight")
frame.key:SetPoint("TOPRIGHT", frame, -18, -16)
frame.key:SetText("+?")
frame.key:SetTextColor(1, 1, 1)

local function MakeTopActionButton(parent, label, xOffset, onClick, width)
    local b = CreateFrame("Button", nil, parent, "BackdropTemplate")
    b:SetSize(width or 18, 18)
    b:SetPoint("TOPRIGHT", parent, xOffset, -13)
    b:SetFrameLevel(parent:GetFrameLevel() + 3)
    b:SetBackdrop({
        bgFile = "Interface\\Buttons\\WHITE8x8",
        edgeFile = "Interface\\Buttons\\WHITE8x8",
        edgeSize = 1,
    })
    b:SetBackdropColor(0.02, 0.025, 0.035, 0.38)
    b:SetBackdropBorderColor(0.70, 0.78, 0.90, 0.28)

    b.text = b:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    b.text:SetPoint("CENTER", b, "CENTER", 0, 0)
    b.text:SetText(label)
    b.text:SetTextColor(0.78, 0.86, 0.95)

    b:SetScript("OnEnter", function(self)
        self:SetBackdropColor(0.07, 0.09, 0.13, 0.75)
        self:SetBackdropBorderColor(0.36, 0.92, 1.00, 0.70)
    end)

    b:SetScript("OnLeave", function(self)
        self:SetBackdropColor(0.02, 0.025, 0.035, 0.38)
        self:SetBackdropBorderColor(0.70, 0.78, 0.90, 0.28)
    end)

    b:SetScript("OnClick", onClick)
    return b
end

frame.readyButton = MakeTopActionButton(frame, "Rdy?", -122, function()
    SafeCall(function()
        DoReadyCheck()
    end)
end, 38)

frame.countdownButton = MakeTopActionButton(frame, "Pull!", -76, function()
    SafeCall(function()
        if C_PartyInfo and C_PartyInfo.DoCountdown then
            C_PartyInfo.DoCountdown(6)
        elseif SlashCmdList and SlashCmdList.COUNTDOWN then
            SlashCmdList.COUNTDOWN("6")
        else
            RunMacroText("/countdown 6")
        end
    end)
end, 42)


frame.time = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalHuge2")
frame.time:SetPoint("TOPLEFT", frame, 18, -38)
frame.time:SetText("00:00")
frame.time:SetTextColor(1, 1, 1)

frame.status = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
frame.status:SetPoint("RIGHT", frame.time, "RIGHT", 132, 0)
frame.status:SetText("-")
frame.status:SetTextColor(0.75, 0.75, 0.75)

frame.stats = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
frame.stats:SetPoint("TOPLEFT", frame, 18, -73)
frame.stats:SetPoint("RIGHT", frame, -18, 0)
frame.stats:SetJustifyH("LEFT")
frame.stats:SetText("Deaths 0  |  Penalty -0s")
frame.stats:SetTextColor(0.78, 0.81, 0.87)

frame.barBG = frame:CreateTexture(nil, "ARTWORK")
frame.barBG:SetColorTexture(1, 1, 1, 0.08)
frame.barBG:SetPoint("TOPLEFT", frame, 18, -108)
frame.barBG:SetPoint("TOPRIGHT", frame, -18, -108)
frame.barBG:SetHeight(4)

frame.bar = frame:CreateTexture(nil, "ARTWORK")
frame.bar:SetPoint("LEFT", frame.barBG, "LEFT", 0, 0)
frame.bar:SetHeight(4)
frame.bar:SetWidth(1)

frame.depletedStamp = frame:CreateTexture(nil, "BACKGROUND", nil, 1)
frame.depletedStamp:SetTexture("Interface\\AddOns\\NaijsMPlusTimer\\Media\\DepletedStamp")
frame.depletedStamp:SetSize(290, 290)
frame.depletedStamp:SetPoint("CENTER", frame, "CENTER", 0, 6)
frame.depletedStamp:SetAlpha(0.22)
frame.depletedStamp:Hide()

frame.objTitle = frame:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
frame.objTitle:SetPoint("TOPLEFT", frame, 18, OBJECTIVE_TOP_Y)
frame.objTitle:SetText("OBJECTIVES")
frame.objTitle:SetTextColor(0.72, 0.77, 0.84)

frame.objectives = {}
for i = 1, 8 do
    local row = CreateFrame("Frame", nil, frame)
    row:SetPoint("TOPLEFT", frame, 18, OBJECTIVE_TOP_Y - 19 - ((i - 1) * OBJECTIVE_ROW_HEIGHT))
    row:SetPoint("RIGHT", frame, -18, 0)
    row:SetHeight(OBJECTIVE_ROW_HEIGHT)

    row.check = row:CreateTexture(nil, "OVERLAY")
    row.check:SetTexture("Interface\\AddOns\\NaijsMPlusTimer\\Media\\CheckGreen")
    row.check:SetSize(14, 14)
    row.check:SetPoint("LEFT", row, "LEFT", 0, 0)
    row.check:Hide()

    row.bullet = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    row.bullet:SetPoint("LEFT", row, "LEFT", 3, 0)
    row.bullet:SetText("•")
    row.bullet:SetTextColor(0.68, 0.72, 0.78)

    row.text = row:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
    row.text:SetPoint("LEFT", row, "LEFT", 20, 0)
    row.text:SetPoint("RIGHT", row, "RIGHT", 0, 0)
    row.text:SetJustifyH("LEFT")
    row.text:SetTextColor(0.88, 0.90, 0.94)
    row.text:SetText("")

    frame.objectives[i] = row
end

local function ApplyVisualSettings()
    local a = NaijsMPlusTimerDB.alpha or 0.50
    frame:SetBackdropColor(0.03, 0.035, 0.05, a)
    frame:SetBackdropBorderColor(
        NaijsMPlusTimerDB.borderR or 0.2118,
        NaijsMPlusTimerDB.borderG or 0.9216,
        NaijsMPlusTimerDB.borderB or 1.0000,
        0.75
    )

    frame.headerGlow:SetColorTexture(
        NaijsMPlusTimerDB.borderR or 0.2118,
        NaijsMPlusTimerDB.borderG or 0.9216,
        NaijsMPlusTimerDB.borderB or 1.0000,
        0.10
    )

    frame:SetScale(NaijsMPlusTimerDB.scale or 1.00)
end

local function SavePosition()
    local point, _, relativePoint, x, y = frame:GetPoint(1)
    NaijsMPlusTimerDB.point = point
    NaijsMPlusTimerDB.relativePoint = relativePoint
    NaijsMPlusTimerDB.x = x
    NaijsMPlusTimerDB.y = y
end

frame:SetScript("OnDragStart", function(self)
    if NaijsMPlusTimerDB.locked then
        return
    end

    self:StartMoving()
end)

frame:SetScript("OnDragStop", function(self)
    if NaijsMPlusTimerDB.locked then
        return
    end

    self:StopMovingOrSizing()
    SavePosition()
end)

local function RestorePosition()
    frame:ClearAllPoints()
    frame:SetPoint(
        NaijsMPlusTimerDB.point or "RIGHT",
        UIParent,
        NaijsMPlusTimerDB.relativePoint or "RIGHT",
        NaijsMPlusTimerDB.x or -24,
        NaijsMPlusTimerDB.y or 0
    )
    ApplyVisualSettings()
end

local blizzTrackerHooked = false

local function HideObjectiveTrackerNow()
    if not ShouldUseNaijsTracker() then
        return
    end

    SafeCall(function()
        local tracker = _G.ObjectiveTrackerFrame
        if tracker then
            tracker:SetAlpha(0)
            tracker:Hide()
        end
    end)

    SafeCall(function()
        local tracker = _G.ScenarioObjectiveTracker
        if tracker then
            tracker:SetAlpha(0)
            tracker:Hide()
        end
    end)

    SafeCall(function()
        local tracker = _G.ScenarioObjectiveTrackerContainer
        if tracker then
            tracker:SetAlpha(0)
            tracker:Hide()
        end
    end)
end

local function HookBlizzardTracker()
    if blizzTrackerHooked then
        return
    end

    local function hookFrame(obj)
        if not obj then
            return
        end

        SafeCall(function()
            hooksecurefunc(obj, "Show", function()
                C_Timer.After(0, HideObjectiveTrackerNow)
                C_Timer.After(0.10, HideObjectiveTrackerNow)
            end)
        end)

        SafeCall(function()
            obj:HookScript("OnShow", function()
                C_Timer.After(0, HideObjectiveTrackerNow)
                C_Timer.After(0.10, HideObjectiveTrackerNow)
            end)
        end)
    end

    hookFrame(SafeCall(function() return _G.ObjectiveTrackerFrame end))
    hookFrame(SafeCall(function() return _G.ScenarioObjectiveTracker end))
    hookFrame(SafeCall(function() return _G.ScenarioObjectiveTrackerContainer end))

    blizzTrackerHooked = true
end

local function GetKeyLevel()
    local level = SafeCall(function()
        if C_ChallengeMode and C_ChallengeMode.GetActiveKeystoneInfo then
            return C_ChallengeMode.GetActiveKeystoneInfo()
        end
    end)

    if level and tonumber(level) then
        return tonumber(level)
    end

    return nil
end

local function FindChallengeTimer()
    local challengeType = Enum and Enum.WorldElapsedTimerTypes and Enum.WorldElapsedTimerTypes.ChallengeMode
    local timers = { SafeCall(function() return GetWorldElapsedTimers() end) }

    for i = 1, #timers do
        local timerID = timers[i]
        local a, b, c = SafeCall(function()
            return GetWorldElapsedTime(timerID)
        end)

        local elapsed
        local timerType

        if c == challengeType then
            elapsed = b
            timerType = c
        elseif b == challengeType then
            elapsed = a
            timerType = b
        end

        if timerType == challengeType then
            return timerID, elapsed or 0
        end
    end

    local startTime = SafeCall(function()
        if C_ChallengeMode and C_ChallengeMode.GetStartTime then
            return C_ChallengeMode.GetStartTime()
        end
    end)

    if startTime and startTime > 0 then
        return nil, GetTime() - startTime
    end

    return nil, 0
end

local function CopyRunInfo(info)
    if not info then
        return nil
    end

    return {
        mapName = info.mapName,
        timeLimit = info.timeLimit or 0,
        elapsed = info.elapsed or 0,
        deaths = info.deaths or 0,
        timeLost = info.timeLost or 0,
        remaining = info.remaining,
        keyLevel = info.keyLevel,
        active = false,
        finished = true,
        preStart = false,
    }
end

local function GetRunInfo()
    local active = IsMPlusActive()
    local useTracker = ShouldUseNaijsTracker()

    if not active and showFinishedRun and finishedRunInfo then
        return finishedRunInfo
    end

    if not active and not useTracker then
        return nil
    end

    local mapID = SafeCall(function()
        if C_ChallengeMode and C_ChallengeMode.GetActiveChallengeMapID then
            return C_ChallengeMode.GetActiveChallengeMapID()
        end
    end)

    local name, _, timeLimit

    if mapID then
        name, _, timeLimit = SafeCall(function()
            return C_ChallengeMode.GetMapUIInfo(mapID)
        end)
    end

    if not name or name == "" then
        name = SafeCall(function()
            local instanceName = GetInstanceInfo()
            return instanceName
        end)
    end

    local deaths, timeLost = 0, 0
    local elapsed = 0

    if active then
        local _, activeElapsed = FindChallengeTimer()
        elapsed = activeElapsed or 0

        deaths, timeLost = SafeCall(function()
            if C_ChallengeMode.GetDeathCount then
                return C_ChallengeMode.GetDeathCount()
            end
        end)

        deaths = deaths or 0
        timeLost = timeLost or 0
        timeLimit = timeLimit or 0

        local info = {
            mapName = name,
            timeLimit = timeLimit,
            elapsed = elapsed,
            deaths = deaths,
            timeLost = timeLost,
            remaining = timeLimit - elapsed,
            keyLevel = GetKeyLevel(),
            active = true,
        }

        lastActiveRunInfo = CopyRunInfo(info)
        return info
    end

    return {
        mapName = name,
        timeLimit = timeLimit or 0,
        elapsed = 0,
        deaths = 0,
        timeLost = 0,
        remaining = nil,
        keyLevel = GetKeyLevel(),
        active = false,
        preStart = true,
    }
end

local function BuildStats(deaths, timeLost)
    return string.format("Deaths %d  |  Penalty -%ds", deaths or 0, timeLost or 0)
end

local function GetScenarioCriteriaCount()
    local count = SafeCall(function()
        if C_ScenarioInfo and C_ScenarioInfo.GetStepInfo then
            local stepInfo = C_ScenarioInfo.GetStepInfo()
            if type(stepInfo) == "table" then
                return stepInfo.numCriteria or stepInfo.criteriaCount or 0
            end
        end
    end)

    if count and count > 0 then
        return count
    end

    count = SafeCall(function()
        if C_Scenario and C_Scenario.GetStepInfo then
            local _, _, numCriteria = C_Scenario.GetStepInfo()
            return numCriteria
        end
    end)

    return count or 0
end

local function NormalizeCriteria(index)
    local data = SafeCall(function()
        if C_ScenarioInfo and C_ScenarioInfo.GetCriteriaInfo then
            return C_ScenarioInfo.GetCriteriaInfo(index)
        end
    end)

    if type(data) == "table" then
        return {
            text = data.description or data.criteriaString or "",
            completed = data.completed,
            quantity = data.quantity or 0,
            totalQuantity = data.totalQuantity or 0,
            quantityString = data.quantityString or "",
            weighted = data.isWeightedProgress,
        }
    end

    local criteriaString, criteriaType, completed, quantity, totalQuantity, flags, assetID, quantityString, criteriaID, duration, elapsed, failed, isWeightedProgress = SafeCall(function()
        if C_Scenario and C_Scenario.GetCriteriaInfo then
            return C_Scenario.GetCriteriaInfo(index)
        end
    end)

    return {
        text = criteriaString or "",
        completed = completed,
        quantity = quantity or 0,
        totalQuantity = totalQuantity or 0,
        quantityString = quantityString or "",
        weighted = isWeightedProgress,
    }
end

local function IsForcesText(text)
    text = string.lower(text or "")
    return text:find("enemy") or text:find("forces") or text:find("streit") or text:find("gegner")
end

local function ParsePercentFromString(text)
    if not text or text == "" then
        return nil
    end

    local value = text:match("([%d%,%.]+)%s*%%")
    if value then
        value = value:gsub(",", ".")
        value = tonumber(value)
        if value then
            return math.max(0, math.min(100, value))
        end
    end

    return nil
end

local function FormatPercentValue(value)
    value = tonumber(value) or 0
    value = math.max(0, math.min(100, value))

    if math.abs(value - math.floor(value + 0.5)) < 0.05 then
        return string.format("%d%%", math.floor(value + 0.5))
    end

    return string.format("%.1f%%", value)
end

local function NormalizeForcesPercent(c)
    -- Correct M+ Enemy Forces handling:
    -- For weighted progress, Blizzard's numeric quantity is already the percentage.
    -- Do NOT divide quantity by totalQuantity here.
    if c.weighted then
        return math.max(0, math.min(100, tonumber(c.quantity) or 0))
    end

    -- If Blizzard provides a formatted percent string, use that.
    local fromString = ParsePercentFromString(c.quantityString)
    if fromString then
        return fromString
    end

    -- Last fallback only for non-weighted criteria.
    local q = tonumber(c.quantity) or 0
    local total = tonumber(c.totalQuantity) or 0

    if total > 0 then
        return math.max(0, math.min(100, (q / total) * 100))
    end

    return math.max(0, math.min(100, q))
end

local function BuildProgressText(c)
    if c.completed then
        return ""
    end

    -- Enemy Forces must use exact weighted-progress logic.
    if IsForcesText(c.text) then
        return FormatPercentValue(NormalizeForcesPercent(c))
    end

    if c.quantityString and c.quantityString ~= "" then
        return c.quantityString
    end

    if c.totalQuantity and c.totalQuantity > 1 then
        return string.format("%d/%d", c.quantity or 0, c.totalQuantity)
    end

    return ""
end

local function ReadObjectives()
    local rows = {}
    local forcesPercent = nil
    local count = GetScenarioCriteriaCount()

    for i = 1, math.min(count, 8) do
        local c = NormalizeCriteria(i)

        if c.text and c.text ~= "" then
            local progress = BuildProgressText(c)

            if IsForcesText(c.text) then
                if c.completed then
                    forcesPercent = 100
                else
                    forcesPercent = NormalizeForcesPercent(c)
                end
            end

            if progress ~= "" then
                progress = "  " .. progress
            end

            table.insert(rows, {
                text = c.text .. progress,
                done = c.completed,
                isForces = IsForcesText(c.text),
            })
        end
    end

    return rows, forcesPercent
end

local function ResizeForObjectives(rowCount)
    local height = MIN_PANEL_HEIGHT

    if rowCount > 0 then
        height = 154 + (rowCount * OBJECTIVE_ROW_HEIGHT)
    end

    frame:SetHeight(height)
end

local currentForcesPercent = nil

local function UpdateObjectives()
    local rows, forcesPercent = ReadObjectives()
    currentForcesPercent = forcesPercent

    ResizeForObjectives(#rows)

    if #rows == 0 then
        frame.objTitle:SetText("")
        for i = 1, #frame.objectives do
            frame.objectives[i].text:SetText("")
            frame.objectives[i].check:Hide()
            frame.objectives[i].bullet:Hide()
        end
        return
    end

    frame.objTitle:SetText("OBJECTIVES")

    for i = 1, #frame.objectives do
        local obj = frame.objectives[i]
        local row = rows[i]

        if row then
            obj.text:SetText(row.text)

            if row.done then
                obj.check:Show()
                obj.bullet:Hide()
                obj.text:SetTextColor(0.50, 1.00, 0.62)
            else
                obj.check:Hide()
                obj.bullet:Show()

                if row.isForces then
                    obj.text:SetTextColor(1.00, 0.86, 0.42)
                    obj.bullet:SetTextColor(1.00, 0.86, 0.42)
                else
                    obj.text:SetTextColor(0.88, 0.90, 0.94)
                    obj.bullet:SetTextColor(0.68, 0.72, 0.78)
                end
            end
        else
            obj.text:SetText("")
            obj.check:Hide()
            obj.bullet:Hide()
        end
    end
end

local function HandleSounds(info)
    if not info then
        lastDeaths = nil
        playedForces100 = false
        playedDepletedSound = false
        return
    end

    if lastDeaths == nil then
        lastDeaths = info.deaths or 0
    elseif info.deaths and info.deaths > lastDeaths then
        PlayConfiguredSound("death")
        lastDeaths = info.deaths
    elseif info.deaths and info.deaths < lastDeaths then
        lastDeaths = info.deaths
    end

    if currentForcesPercent and currentForcesPercent >= 100 then
        if not playedForces100 then
            PlayConfiguredSound("forces")
            playedForces100 = true
        end
    elseif currentForcesPercent and currentForcesPercent < 100 then
        playedForces100 = false
    end

    if info.active and info.remaining and info.remaining <= 0 then
        if not playedDepletedSound then
            PlayConfiguredSound("depleted")
            playedDepletedSound = true
        end
    elseif info.remaining and info.remaining > 0 then
        playedDepletedSound = false
    end
end

local function UpdateDisplay()
    HookBlizzardTracker()
    HideObjectiveTrackerNow()

    local info = GetRunInfo()

    if not info then
        frame:Hide()
        frame.depletedStamp:Hide()
        HandleSounds(nil)
        SafeCall(function()
            local tracker = _G.ObjectiveTrackerFrame
            if tracker then
                tracker:SetAlpha(1)
            end
        end)
        SafeCall(function()
            local tracker = _G.ScenarioObjectiveTracker
            if tracker then
                tracker:SetAlpha(1)
            end
        end)
        SafeCall(function()
            local tracker = _G.ScenarioObjectiveTrackerContainer
            if tracker then
                tracker:SetAlpha(1)
            end
        end)
        return
    end

    frame:Show()
    ApplyVisualSettings()

    local status, pct = UpgradeStatus(info.remaining, info.timeLimit)
    local r, g, b = StatusColor(status)

    if info.preStart then
        frame.time:SetText("READY")
        frame.status:SetText("")
        frame.depletedStamp:Hide()
        pct = 1
    else
        frame.time:SetText(FormatClock(info.remaining))

        if status == "DEPLETED" then
            frame.status:SetText("")
            frame.depletedStamp:Show()
        else
            frame.status:SetText(status)
            frame.status:SetTextColor(r, g, b)
            frame.depletedStamp:Hide()
        end
    end

    frame.bar:SetColorTexture(
        NaijsMPlusTimerDB.barR or 0.5490,
        NaijsMPlusTimerDB.barG or 0.8510,
        NaijsMPlusTimerDB.barB or 1.0000,
        0.92
    )
    if info.finished then
        frame.stats:SetText(string.format("Deaths %d  |  Penalty -%ds  |  Used %s", info.deaths or 0, info.timeLost or 0, FormatClock(info.elapsed or 0)))
    else
        frame.stats:SetText(BuildStats(info.deaths, info.timeLost))
    end

    if info.keyLevel then
        frame.key:SetText("+" .. info.keyLevel)
    else
        frame.key:SetText("+?")
    end

    local barMax = frame.barBG:GetWidth() or 1
    frame.bar:SetWidth(math.max(1, barMax * math.max(0, math.min(1, pct or 0))))

    UpdateObjectives()
    HandleSounds(info)
end

local settings = CreateFrame("Frame", "NaijsMPlusTimerSettings", UIParent, "BackdropTemplate")
settings:SetSize(360, 570)
settings:SetPoint("CENTER")
settings:SetFrameStrata("DIALOG")
settings:SetFrameLevel(200)
settings:SetMovable(true)
settings:EnableMouse(true)
settings:RegisterForDrag("LeftButton")
settings:SetBackdrop({
    bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background-Dark",
    edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
    tile = true,
    tileSize = 16,
    edgeSize = 14,
    insets = { left = 4, right = 4, top = 4, bottom = 4 },
})
settings:SetBackdropColor(0.03, 0.035, 0.05, 0.98)
settings:SetBackdropBorderColor(0.52, 0.60, 0.74, 0.90)
settings:Hide()
table.insert(UISpecialFrames, "NaijsMPlusTimerSettings")

settings:SetScript("OnDragStart", function(self) self:StartMoving() end)
settings:SetScript("OnDragStop", function(self) self:StopMovingOrSizing() end)

settings.title = settings:CreateFontString(nil, "OVERLAY", "GameFontNormalLarge")
settings.title:SetPoint("TOPLEFT", settings, 18, -18)
settings.title:SetText("Naijs M+ Timer Settings")

settings.close = CreateFrame("Button", nil, settings, "UIPanelCloseButton")
settings.close:SetPoint("TOPRIGHT", settings, -4, -4)

local function MakeSlider(name, label, minVal, maxVal, step, y, getValue, setValue)
    local s = CreateFrame("Slider", name, settings, "OptionsSliderTemplate")
    s:SetPoint("TOPLEFT", settings, 32, y)
    s:SetWidth(260)
    s:SetMinMaxValues(minVal, maxVal)
    s:SetValueStep(step)
    s:SetObeyStepOnDrag(true)
    s:SetValue(getValue())
    s.Text:SetText(label .. ": " .. string.format("%.2f", getValue()))
    s.Low:SetText(tostring(minVal))
    s.High:SetText(tostring(maxVal))

    s:SetScript("OnValueChanged", function(self, value)
        value = math.floor((value / step) + 0.5) * step
        setValue(value)
        self.Text:SetText(label .. ": " .. string.format("%.2f", value))
        ApplyVisualSettings()
    end)

    return s
end

local scaleSlider = MakeSlider(
    "NaijsMPlusTimerScaleSlider",
    "Size",
    0.70,
    1.60,
    0.05,
    -66,
    function() return NaijsMPlusTimerDB.scale or 1.00 end,
    function(v) NaijsMPlusTimerDB.scale = v end
)

local alphaSlider = MakeSlider(
    "NaijsMPlusTimerAlphaSlider",
    "Transparency",
    0.30,
    1.00,
    0.05,
    -121,
    function() return NaijsMPlusTimerDB.alpha or 0.50 end,
    function(v) NaijsMPlusTimerDB.alpha = v end
)

local deathVolumeSlider = MakeSlider(
    "NaijsMPlusTimerDeathVolumeSlider",
    "Death Sound Boost",
    0.00,
    1.00,
    0.05,
    -176,
    function() return NaijsMPlusTimerDB.deathVolume or 0.50 end,
    function(v) NaijsMPlusTimerDB.deathVolume = v end
)

local forcesVolumeSlider = MakeSlider(
    "NaijsMPlusTimerForcesVolumeSlider",
    "100% Sound Boost",
    0.00,
    1.00,
    0.05,
    -231,
    function() return NaijsMPlusTimerDB.forcesVolume or 0.50 end,
    function(v) NaijsMPlusTimerDB.forcesVolume = v end
)

local depletedVolumeSlider = MakeSlider(
    "NaijsMPlusTimerDepletedVolumeSlider",
    "Depleted Sound Boost",
    0.00,
    1.00,
    0.05,
    -286,
    function() return NaijsMPlusTimerDB.depletedVolume or 0.50 end,
    function(v) NaijsMPlusTimerDB.depletedVolume = v end
)

local function OpenColorPicker(kind)
    local r, g, b

    if kind == "border" then
        r, g, b = NaijsMPlusTimerDB.borderR, NaijsMPlusTimerDB.borderG, NaijsMPlusTimerDB.borderB
    else
        r, g, b = NaijsMPlusTimerDB.barR, NaijsMPlusTimerDB.barG, NaijsMPlusTimerDB.barB
    end

    r = r or 1
    g = g or 1
    b = b or 1

    local old = { r = r, g = g, b = b }

    local function changed()
        local nr, ng, nb = ColorPickerFrame:GetColorRGB()
        if kind == "border" then
            NaijsMPlusTimerDB.borderR = nr
            NaijsMPlusTimerDB.borderG = ng
            NaijsMPlusTimerDB.borderB = nb
        else
            NaijsMPlusTimerDB.barR = nr
            NaijsMPlusTimerDB.barG = ng
            NaijsMPlusTimerDB.barB = nb
        end
        ApplyVisualSettings()
    end

    ColorPickerFrame:SetupColorPickerAndShow({
        r = r,
        g = g,
        b = b,
        swatchFunc = changed,
        cancelFunc = function()
            if kind == "border" then
                NaijsMPlusTimerDB.borderR = old.r
                NaijsMPlusTimerDB.borderG = old.g
                NaijsMPlusTimerDB.borderB = old.b
            else
                NaijsMPlusTimerDB.barR = old.r
                NaijsMPlusTimerDB.barG = old.g
                NaijsMPlusTimerDB.barB = old.b
            end
            ApplyVisualSettings()
        end,
    })
end

local function MakeButton(text, x, y, width, onClick)
    local b = CreateFrame("Button", nil, settings, "UIPanelButtonTemplate")
    b:SetSize(width or 130, 24)
    b:SetPoint("TOPLEFT", settings, x, y)
    b:SetText(text)
    b:SetScript("OnClick", onClick)
    return b
end

MakeButton("Border Color", 28, -335, 140, function()
    OpenColorPicker("border")
end)

MakeButton("Bar Color", 190, -335, 130, function()
    OpenColorPicker("bar")
end)

local deathCheck = CreateFrame("CheckButton", nil, settings, "UICheckButtonTemplate")
deathCheck:SetPoint("TOPLEFT", settings, 24, -375)
deathCheck.text = deathCheck:CreateFontString(nil, "OVERLAY", "GameFontNormal")
deathCheck.text:SetPoint("LEFT", deathCheck, "RIGHT", 0, 1)
deathCheck.text:SetText("Sound bei Tod")
deathCheck:SetScript("OnClick", function(self)
    NaijsMPlusTimerDB.deathSound = self:GetChecked()
end)

local forcesCheck = CreateFrame("CheckButton", nil, settings, "UICheckButtonTemplate")
forcesCheck:SetPoint("TOPLEFT", settings, 24, -409)
forcesCheck.text = forcesCheck:CreateFontString(nil, "OVERLAY", "GameFontNormal")
forcesCheck.text:SetPoint("LEFT", forcesCheck, "RIGHT", 0, 1)
forcesCheck.text:SetText("Sound bei 100% Forces")
forcesCheck:SetScript("OnClick", function(self)
    NaijsMPlusTimerDB.forcesSound = self:GetChecked()
end)

local depletedCheck = CreateFrame("CheckButton", nil, settings, "UICheckButtonTemplate")
depletedCheck:SetPoint("TOPLEFT", settings, 24, -443)
depletedCheck.text = depletedCheck:CreateFontString(nil, "OVERLAY", "GameFontNormal")
depletedCheck.text:SetPoint("LEFT", depletedCheck, "RIGHT", 0, 1)
depletedCheck.text:SetText("Sound bei Depleted")
depletedCheck:SetScript("OnClick", function(self)
    NaijsMPlusTimerDB.depletedSound = self:GetChecked()
end)

local lockCheck = CreateFrame("CheckButton", nil, settings, "UICheckButtonTemplate")
lockCheck:SetPoint("TOPLEFT", settings, 24, -477)
lockCheck.text = lockCheck:CreateFontString(nil, "OVERLAY", "GameFontNormal")
lockCheck.text:SetPoint("LEFT", lockCheck, "RIGHT", 0, 1)
lockCheck.text:SetText("Lock Window")
lockCheck:SetScript("OnClick", function(self)
    NaijsMPlusTimerDB.locked = self:GetChecked()
end)

MakeButton("Test Death Sound", 28, -508, 140, function()
    NaijsMPlusTimerDB.deathVolume = deathVolumeSlider:GetValue() or NaijsMPlusTimerDB.deathVolume or 0.50
    PlayConfiguredSound("death")
end)

MakeButton("Test 100% Sound", 190, -508, 130, function()
    NaijsMPlusTimerDB.forcesVolume = forcesVolumeSlider:GetValue() or NaijsMPlusTimerDB.forcesVolume or 0.50
    PlayConfiguredSound("forces")
end)

MakeButton("Test Depleted", 108, -539, 140, function()
    NaijsMPlusTimerDB.depletedVolume = depletedVolumeSlider:GetValue() or NaijsMPlusTimerDB.depletedVolume or 0.50
    PlayConfiguredSound("depleted")
end)

local function RefreshSettings()
    settings:SetBackdropBorderColor(
        NaijsMPlusTimerDB.borderR or 0.2118,
        NaijsMPlusTimerDB.borderG or 0.9216,
        NaijsMPlusTimerDB.borderB or 1.0000,
        0.90
    )

    scaleSlider:SetValue(NaijsMPlusTimerDB.scale or 1.00)
    alphaSlider:SetValue(NaijsMPlusTimerDB.alpha or 0.50)
    deathVolumeSlider:SetValue(NaijsMPlusTimerDB.deathVolume or 0.50)
    forcesVolumeSlider:SetValue(NaijsMPlusTimerDB.forcesVolume or 0.50)
    depletedVolumeSlider:SetValue(NaijsMPlusTimerDB.depletedVolume or 0.50)
    deathCheck:SetChecked(NaijsMPlusTimerDB.deathSound)
    forcesCheck:SetChecked(NaijsMPlusTimerDB.forcesSound)
    depletedCheck:SetChecked(NaijsMPlusTimerDB.depletedSound)
    lockCheck:SetChecked(NaijsMPlusTimerDB.locked)
end

local function ToggleSettings()
    if settings:IsShown() then
        settings:Hide()
    else
        RefreshSettings()
        settings:Show()
    end
end

SLASH_NAIJSMPLUSTIMERSETTINGS1 = "/m+"
SlashCmdList.NAIJSMPLUSTIMERSETTINGS = function()
    ToggleSettings()
end

local ticker = 0
frame:SetScript("OnUpdate", function(_, elapsed)
    ticker = ticker + elapsed
    if ticker >= 0.15 then
        ticker = 0
        UpdateDisplay()
    end
end)

local events = CreateFrame("Frame")
events:RegisterEvent("ADDON_LOADED")
events:RegisterEvent("PLAYER_ENTERING_WORLD")
events:RegisterEvent("ZONE_CHANGED_NEW_AREA")
events:RegisterEvent("PLAYER_DIFFICULTY_CHANGED")
events:RegisterEvent("SCENARIO_UPDATE")
events:RegisterEvent("SCENARIO_POI_UPDATE")
events:RegisterEvent("CHALLENGE_MODE_START")
events:RegisterEvent("CHALLENGE_MODE_RESET")
events:RegisterEvent("CHALLENGE_MODE_COMPLETED")
events:RegisterEvent("CHALLENGE_MODE_DEATH_COUNT_UPDATED")
events:RegisterEvent("SCENARIO_CRITERIA_UPDATE")
events:RegisterEvent("CRITERIA_UPDATE")
events:RegisterEvent("WORLD_STATE_TIMER_START")
events:RegisterEvent("WORLD_STATE_TIMER_STOP")

events:SetScript("OnEvent", function(_, event, arg1)
    if event == "ADDON_LOADED" and arg1 == ADDON_NAME then
        ApplyDefaults()
        RestorePosition()
        frame:Hide()
        C_Timer.After(0.5, HookBlizzardTracker)
        return
    end

    if event == "CHALLENGE_MODE_START" then
        lastDeaths = nil
        playedForces100 = false
        playedDepletedSound = false
        lastActiveRunInfo = nil
        finishedRunInfo = nil
        showFinishedRun = false
    end

    if event == "CHALLENGE_MODE_COMPLETED" then
        if lastActiveRunInfo then
            finishedRunInfo = CopyRunInfo(lastActiveRunInfo)
            showFinishedRun = true
        end
        lastDeaths = nil
        playedForces100 = false
        playedDepletedSound = false
    end

    if event == "CHALLENGE_MODE_RESET" then
        lastDeaths = nil
        playedForces100 = false
        playedDepletedSound = false

        if not showFinishedRun then
            lastActiveRunInfo = nil
            finishedRunInfo = nil
        end
    end

    if event == "PLAYER_ENTERING_WORLD" or event == "ZONE_CHANGED_NEW_AREA" then
        if not IsPartyInstance() then
            lastActiveRunInfo = nil
            finishedRunInfo = nil
            showFinishedRun = false
        end
    end

    C_Timer.After(0.02, UpdateDisplay)
    C_Timer.After(0.50, UpdateDisplay)
    C_Timer.After(1.50, UpdateDisplay)
    C_Timer.After(3.00, UpdateDisplay)
end)

print("|cff66ff66NaijsMPlusTimer loaded.|r /m+ for Settings")
