local ADDON_NAME, RPAL = ...
if type(RPAL) ~= "table" then return end

-- ============================================================
-- ODYN custom sound layer
-- ============================================================
-- WoW does not expose reliable per-file pitch/volume controls to addons.
-- ODYN therefore ships small pre-rendered variants so these sliders affect
-- only ODYN, never the player's global WoW SFX volume.

local ROOT = "Interface\\AddOns\\ODYN\\Media\\Sounds\\"
local DEFAULT_VOLUME = 100
local VOLUME_STEP = 10
local KINDS = { mention=true, target=true, achievement=true, levelup=true, renown=true }
local FILE_STEMS = {
    mention="mention",
    target="target",
    achievement="achievement",
    levelup="levelup",
}
local runtime = RPAL._odynSoundRuntime or {renownCounter=0,renownPitch=0,lastRenownAt=-100}
RPAL._odynSoundRuntime = runtime

local function Settings()
    if type(RPAL.db)=="table" and type(RPAL.db.settings)=="table" then return RPAL.db.settings end
    if RPAL.GetSettings then return RPAL.GetSettings() end
    return nil
end

local function NormalizeKind(kind)
    kind=tostring(kind or ""):lower()
    if kind=="emote" or kind=="emoted" or kind=="interaction" then kind="target" end
    if kind=="level" or kind=="level_up" then kind="levelup" end
    return KINDS[kind] and kind or nil
end

local function ClampVolume(value)
    value=tonumber(value) or DEFAULT_VOLUME
    value=math.floor((value/VOLUME_STEP)+0.5)*VOLUME_STEP
    if value<10 then value=10 end
    if value>100 then value=100 end
    return value
end

local function EnsureSettings()
    local settings=Settings()
    if not settings then return nil,nil end
    if type(settings.odynSoundVolumes)~="table" then settings.odynSoundVolumes={} end
    if type(settings.odynSoundMuted)~="table" then settings.odynSoundMuted={} end

    -- HOTFIX22: the original custom-audio release defaulted every channel to
    -- 80%. The shipped assets are quieter than expected in-game, so migrate
    -- only untouched 80% defaults to 100%. Any value the player deliberately
    -- changed to 10-70/90-100 remains exactly as chosen.
    local version=tonumber(settings.odynSoundSettingsVersion) or 0
    if version<2 then
        for kind in pairs(KINDS) do
            if settings.odynSoundVolumes[kind]==nil or tonumber(settings.odynSoundVolumes[kind])==80 then
                settings.odynSoundVolumes[kind]=100
            end
        end
    end

    for kind in pairs(KINDS) do
        if settings.odynSoundVolumes[kind]==nil then settings.odynSoundVolumes[kind]=DEFAULT_VOLUME end
        settings.odynSoundVolumes[kind]=ClampVolume(settings.odynSoundVolumes[kind])
        if settings.odynSoundMuted[kind]==nil then settings.odynSoundMuted[kind]=false end
        settings.odynSoundMuted[kind]=settings.odynSoundMuted[kind] and true or false
    end
    settings.odynSoundSettingsVersion=2
    return settings.odynSoundVolumes,settings.odynSoundMuted
end

function RPAL.GetODYNSoundVolume(kind)
    kind=NormalizeKind(kind);if not kind then return DEFAULT_VOLUME end
    local volumes=EnsureSettings();return volumes and ClampVolume(volumes[kind]) or DEFAULT_VOLUME
end

function RPAL.SetODYNSoundVolume(kind,value)
    kind=NormalizeKind(kind);if not kind then return false end
    local volumes=EnsureSettings();if not volumes then return false end
    volumes[kind]=ClampVolume(value)
    return true
end

function RPAL.IsODYNSoundMuted(kind)
    kind=NormalizeKind(kind);if not kind then return false end
    local _,muted=EnsureSettings();return muted and muted[kind]==true or false
end

function RPAL.SetODYNSoundMuted(kind,value)
    kind=NormalizeKind(kind);if not kind then return false end
    local _,muted=EnsureSettings();if not muted then return false end
    muted[kind]=value and true or false
    return true
end

function RPAL.GetODYNSoundDefaultVolume()
    return DEFAULT_VOLUME
end

local function FilePath(kind,volume,pitchIndex)
    volume=ClampVolume(volume)
    if kind=="renown" then
        pitchIndex=math.max(1,math.min(6,math.floor(tonumber(pitchIndex) or 1)))
        return ROOT.."renown\\renown_p"..string.format("%02d",pitchIndex).."_v"..string.format("%03d",volume)..".mp3"
    end
    local stem=FILE_STEMS[kind]
    if not stem then return nil end
    return ROOT..stem.."\\"..stem.."_v"..string.format("%03d",volume)..".mp3"
end

function RPAL.PlayODYNSound(kind,options)
    kind=NormalizeKind(kind);if not kind then return false end
    options=type(options)=="table" and options or {}
    if not options.ignoreMute and RPAL.IsODYNSoundMuted(kind) then return false end
    local volume=ClampVolume(options.volume or RPAL.GetODYNSoundVolume(kind))
    local path=FilePath(kind,volume,options.pitchIndex)
    if not path or type(PlaySoundFile)~="function" then return false end
    local ok,handle=pcall(PlaySoundFile,path,"SFX")
    if not ok then return false end
    return handle~=false
end

function RPAL.PreviewODYNSound(kind)
    return RPAL.PlayODYNSound(kind)
end

-- QA helper only. This resets the ephemeral cadence/pitch cursor, never any
-- character progression or SavedVariables. A /odyn test renown N run therefore
-- always demonstrates an exact 3/6/9/... sound cadence from its first +1.
function RPAL.ResetRenownGainSoundCycle()
    runtime.renownCounter=0
    runtime.renownPitch=0
    runtime.lastRenownAt=-100
end

-- Called for logical +1 Renown ticks. Normal streams play exactly once per
-- three Renown. Very large compressed streams get a tiny safety debounce so
-- dozens of 48 ms samples cannot all start in the same frame.
function RPAL.AdvanceRenownGainSound(amount)
    amount=math.max(0,math.floor(tonumber(amount) or 0))
    if amount<=0 then return false end
    local played=false
    for _=1,amount do
        runtime.renownCounter=(tonumber(runtime.renownCounter) or 0)+1
        if runtime.renownCounter>=3 then
            runtime.renownCounter=runtime.renownCounter-3
            local now=(GetTime and GetTime()) or 0
            if now-(tonumber(runtime.lastRenownAt) or -100)>=0.065 then
                runtime.renownPitch=((tonumber(runtime.renownPitch) or 0)%6)+1
                if RPAL.PlayODYNSound("renown",{pitchIndex=runtime.renownPitch}) then
                    runtime.lastRenownAt=now
                    played=true
                end
            end
        end
    end
    return played
end

-- Settings page builder lives here to keep UI.lua below Lua's per-chunk local
-- variable limit. UI.lua passes its normal ODYN helpers so this still looks
-- exactly like the rest of Settings.
function RPAL.BuildSoundSettingsPage(page,helpers)
    if not page or type(helpers)~="table" then return end
    local Font=helpers.Font
    local Check=helpers.Check
    local Button=helpers.Button
    local Section=helpers.Section
    if type(Font)~="function" or type(Check)~="function" or type(Button)~="function" or type(Section)~="function" then return end
    local PALE=helpers.PALE or {0.96,0.93,0.86}
    local MUTED=helpers.MUTED or {0.65,0.67,0.70}
    local GOLD=helpers.GOLD or {0.96,0.76,0.30}

    EnsureSettings()
    Section(page,"Notification & Progression Audio",-74)
    local intro=Font(page,"Each ODYN sound has its own volume and mute state. Defaults start at 100%, without changing WoW's global SFX volume.",9,MUTED)
    intro:SetPoint("TOPLEFT",4,-101);intro:SetWidth(552);intro:SetWordWrap(true)

    local rows={
        {"mention","NAME MENTION","When your character name or a valid TRP nickname / alias is mentioned."},
        {"target","EMOTED AT","When another player directs a /nod, /peer, /shy or similar text emote at you."},
        {"achievement","ACHIEVEMENT UNLOCK","Plays as an achievement unlock toast reaches the screen."},
        {"levelup","LEVEL UP","Plays when Renown progression raises your ODYN level."},
        {"renown","RENOWN GAIN","A short tick every 3 Renown; pitch alternates slightly to reduce repetition fatigue."},
    }

    local function MakeRow(def,y)
        local kind,label,description=def[1],def[2],def[3]
        local title=Font(page,label,10.2,PALE);title:SetPoint("TOPLEFT",4,y)
        local value=Font(page,tostring(RPAL.GetODYNSoundVolume(kind)).."%",10,GOLD);value:SetPoint("TOPLEFT",218,y)
        local desc=Font(page,description,8.15,MUTED);desc:SetPoint("TOPLEFT",4,y-18);desc:SetWidth(244);desc:SetWordWrap(true)

        local slider=CreateFrame("Slider",nil,page,"OptionsSliderTemplate")
        slider:SetPoint("TOPLEFT",4,y-50);slider:SetSize(244,16);slider:SetMinMaxValues(10,100);slider:SetValueStep(10)
        if slider.SetObeyStepOnDrag then slider:SetObeyStepOnDrag(true) end
        slider:SetScript("OnValueChanged",function(self,newValue)
            newValue=ClampVolume(newValue)
            value:SetText(tostring(newValue).."%")
            if not self._odynInitial then RPAL.SetODYNSoundVolume(kind,newValue) end
        end)
        slider._odynInitial=true;slider:SetValue(RPAL.GetODYNSoundVolume(kind));slider._odynInitial=false

        local mute=Check(page,"Mute")
        mute:SetSize(108,30);mute:SetPoint("TOPLEFT",286,y-28);mute:SetChecked(RPAL.IsODYNSoundMuted(kind))
        mute:SetScript("OnClick",function(self)
            -- CheckButton has already toggled itself by the time OnClick runs.
            -- Store that real state instead of inverting it back.
            RPAL.SetODYNSoundMuted(kind,self:GetChecked()==true)
        end)

        local preview=Button(page,"Preview",126,30,false)
        preview:SetPoint("TOPLEFT",426,y-28)
        preview:SetScript("OnClick",function()RPAL.PreviewODYNSound(kind)end)
    end

    for index,def in ipairs(rows) do MakeRow(def,-132-((index-1)*86)) end
    local note=Font(page,"Mute preserves the slider value. Unmuting restores the exact volume you had before.",8.3,MUTED)
    note:SetPoint("TOPLEFT",4,-548);note:SetWidth(552);note:SetWordWrap(true)
end
