MudSystemWorldSounds = {
    path = g_currentModDirectory or "",
    definitions = {
        pressure = {file="sounds/pressure_inflate_loop.ogg", volume=0.68, indoorVolume=0.20, inner=4, outer=24, loop=true, indoorLowpassGain=0.35, indoorLowpassCutoff=450},
        puncture = {file="sounds/puncture_occurs.ogg", volume=0.95, indoorVolume=0.35, inner=6, outer=36, indoorLowpassGain=0.45, indoorLowpassCutoff=700},
        jackPlace = {file="sounds/jack_place.ogg", volume=0.86, indoorVolume=0.24, inner=4, outer=26, indoorLowpassGain=0.40, indoorLowpassCutoff=500},
        jackWork = {file="sounds/jack_raise_loop.ogg", volume=0.72, indoorVolume=0.22, inner=4, outer=24, loop=true, indoorLowpassGain=0.45, indoorLowpassCutoff=500},
        wheelRemove = {file="sounds/wheel_remove_5s.ogg", volume=0.86, indoorVolume=0.24, inner=4, outer=25, stopAfterMs=5000, indoorLowpassGain=0.40, indoorLowpassCutoff=500},
        wheelPlace = {file="sounds/wheel_place_ground.ogg", volume=0.88, indoorVolume=0.24, inner=4, outer=25, indoorLowpassGain=0.40, indoorLowpassCutoff=500},
        wheelRepair = {file="sounds/wheel_repair_5s.ogg", volume=0.84, indoorVolume=0.24, inner=4, outer=23, stopAfterMs=5000, indoorLowpassGain=0.40, indoorLowpassCutoff=500},
        wheelInstall = {file="sounds/wheel_install_5s.ogg", volume=0.86, indoorVolume=0.24, inner=4, outer=25, stopAfterMs=5000, indoorLowpassGain=0.40, indoorLowpassCutoff=500}
    }
}

MudSystemWorldSounds.oneShots = {}
MudSystemWorldSounds.loopOwners = setmetatable({}, {__mode="k"})
MudSystemWorldSounds.isIndoor = false

local function soundNodeExists(node)
    if type(node) ~= "number" or node == 0 then return false end
    if entityExists == nil then return true end
    local ok, exists = pcall(entityExists, node)
    return ok and exists == true
end

local function soundTime()
    return tonumber(g_time) or (g_currentMission ~= nil and tonumber(g_currentMission.time)) or 0
end

local function soundRootNode()
    if g_currentMission ~= nil and soundNodeExists(g_currentMission.terrainRootNode) then
        return g_currentMission.terrainRootNode
    end
    if soundNodeExists(g_terrainNode) then return g_terrainNode end
    return getRootNode ~= nil and getRootNode() or nil
end

function MudSystemWorldSounds:deleteSource(source, stopFirst)
    if source == nil then return end
    if stopFirst == true and source.sample ~= nil and source.sample ~= 0 and stopSample ~= nil then
        pcall(stopSample, source.sample, 0, 0)
    end
    if soundNodeExists(source.node) and delete ~= nil then pcall(delete, source.node) end
    source.node = nil
    source.sample = nil
end

function MudSystemWorldSounds:getIndoorState()
    if g_soundManager ~= nil and g_soundManager.getIsIndoor ~= nil then
        return g_soundManager:getIsIndoor() == true
    end
    return false
end

function MudSystemWorldSounds:applyEnvironmentToSource(source, isIndoor)
    if source == nil or source.sample == nil or source.sample == 0 then return end
    local definition = self.definitions[source.soundKey]
    if definition == nil then return end

    local volume = definition.volume or 1
    if isIndoor == true then
        volume = definition.indoorVolume or volume*0.3
    end
    if setSampleVolume ~= nil then pcall(setSampleVolume, source.sample, volume) end

    if setSampleFrequencyFilter ~= nil then
        local lowpassGain = isIndoor == true and (definition.indoorLowpassGain or 0.4) or 1
        local cutoff = isIndoor == true and (definition.indoorLowpassCutoff or 500) or 20000
        local resonance = isIndoor == true and (definition.indoorLowpassResonance or 1.5) or 1
        pcall(setSampleFrequencyFilter, source.sample, 1, lowpassGain, 0, cutoff, 0, resonance)
    end
    source.currentVolume = volume
    source.isIndoor = isIndoor == true
end

function MudSystemWorldSounds:onIndoorStateChanged(isIndoor)
    isIndoor = isIndoor == true
    if self.isIndoor == isIndoor then return end
    self.isIndoor = isIndoor

    for _, source in ipairs(self.oneShots) do
        self:applyEnvironmentToSource(source, isIndoor)
    end
    for _, loops in pairs(self.loopOwners) do
        for _, source in pairs(loops) do
            self:applyEnvironmentToSource(source, isIndoor)
        end
    end
end

function MudSystemWorldSounds:createSource(soundKey, linkNode, position)
    if g_client == nil or createAudioSource == nil or getAudioSourceSample == nil
        or playSample == nil then return nil end
    local definition = self.definitions[soundKey]
    if definition == nil then return nil end

    local filename = Utils.getFilename(definition.file, self.path)
    if fileExists ~= nil and not fileExists(filename) then
        self._missingLogged = self._missingLogged or {}
        if self._missingLogged[soundKey] ~= true then
            print(string.format("[MudSystemPhysics] Sound file missing: %s", tostring(filename)))
            self._missingLogged[soundKey] = true
        end
        return nil
    end

    local loops = definition.loop == true and 0 or 1
    local node = createAudioSource(
        "mudSystem_" .. tostring(soundKey), filename,
        definition.outer or 25, definition.inner or 4,
        definition.volume or 1, loops
    )
    if not soundNodeExists(node) then return nil end
    local sample = getAudioSourceSample(node)
    if sample == nil or sample == 0 then
        pcall(delete, node)
        return nil
    end

    if setAudioSourceAutoPlay ~= nil then pcall(setAudioSourceAutoPlay, node, false) end
    if setSampleGroup ~= nil and AudioGroup ~= nil and AudioGroup.ENVIRONMENT ~= nil then
        pcall(setSampleGroup, sample, AudioGroup.ENVIRONMENT)
    end
    if soundNodeExists(linkNode) then
        link(linkNode, node)
        setTranslation(node, 0, 0, 0)
    else
        local root = soundRootNode()
        if not soundNodeExists(root) then
            pcall(delete, node)
            return nil
        end
        link(root, node)
        if position ~= nil then
            setWorldTranslation(node, position.x or 0, position.y or 0, position.z or 0)
        else
            setTranslation(node, 0, 0, 0)
        end
    end

    local source = {
        node=node,
        sample=sample,
        linkNode=linkNode,
        soundKey=soundKey,
        loop=definition.loop == true
    }
    self:applyEnvironmentToSource(source, self:getIndoorState())
    playSample(sample, loops, source.currentVolume or definition.volume or 1, 0, 0, 0)
    return source
end

function MudSystemWorldSounds:playOneShotAtNode(soundKey, node)
    local source = self:createSource(soundKey, node, nil)
    if source == nil then return end
    local definition = self.definitions[soundKey] or {}
    local duration = getSampleDuration ~= nil and tonumber(getSampleDuration(source.sample)) or nil
    source.deleteTime = soundTime() + (definition.stopAfterMs
        or (math.max(250, duration or 5000) + 250))
    self.oneShots[#self.oneShots+1] = source
end

function MudSystemWorldSounds:playOneShotAtPosition(soundKey, x, y, z)
    local source = self:createSource(soundKey, nil, {x=x, y=y, z=z})
    if source == nil then return end
    local definition = self.definitions[soundKey] or {}
    local duration = getSampleDuration ~= nil and tonumber(getSampleDuration(source.sample)) or nil
    source.deleteTime = soundTime() + (definition.stopAfterMs
        or (math.max(250, duration or 5000) + 250))
    self.oneShots[#self.oneShots+1] = source
end

function MudSystemWorldSounds:startLoop(owner, tag, soundKey, node, position)
    if g_client == nil or owner == nil or tag == nil then return end
    local loops = self.loopOwners[owner]
    if loops == nil then
        loops = {}
        self.loopOwners[owner] = loops
    end
    local existing = loops[tag]
    if existing ~= nil and soundNodeExists(existing.node)
        and existing.soundKey == soundKey and existing.linkNode == node then return end
    if existing ~= nil then self:deleteSource(existing, true) end
    loops[tag] = self:createSource(soundKey, node, position)
end

function MudSystemWorldSounds:stopLoop(owner, tag)
    local loops = owner ~= nil and self.loopOwners[owner] or nil
    local source = loops ~= nil and loops[tag] or nil
    if source ~= nil then self:deleteSource(source, true) end
    if loops ~= nil then
        loops[tag] = nil
        if next(loops) == nil then self.loopOwners[owner] = nil end
    end
end

function MudSystemWorldSounds:stopOwner(owner)
    local loops = owner ~= nil and self.loopOwners[owner] or nil
    for _, source in pairs(loops or {}) do self:deleteSource(source, true) end
    if owner ~= nil then self.loopOwners[owner] = nil end
end

function MudSystemWorldSounds:update()
    local isIndoor = self:getIndoorState()
    if isIndoor ~= self.isIndoor then self:onIndoorStateChanged(isIndoor) end

    local now = soundTime()
    for index=#self.oneShots,1,-1 do
        local source = self.oneShots[index]
        if not soundNodeExists(source.node) or now >= (source.deleteTime or 0) then
            self:deleteSource(source, false)
            table.remove(self.oneShots, index)
        end
    end
    for owner, loops in pairs(self.loopOwners) do
        for tag, source in pairs(loops) do
            if source == nil or not soundNodeExists(source.node) then loops[tag] = nil end
        end
        if next(loops) == nil then self.loopOwners[owner] = nil end
    end
end

function MudSystemWorldSounds:loadMap()
    self.isIndoor = self:getIndoorState()
    if g_soundManager ~= nil and g_soundManager.addIndoorStateChangedListener ~= nil
        and self._indoorListenerRegistered ~= true then
        g_soundManager:addIndoorStateChangedListener(self)
        self._indoorListenerRegistered = true
    end
end

function MudSystemWorldSounds:deleteMap()
    if self._indoorListenerRegistered == true and g_soundManager ~= nil
        and g_soundManager.removeIndoorStateChangedListener ~= nil then
        g_soundManager:removeIndoorStateChangedListener(self)
    end
    self._indoorListenerRegistered = false
    for _, source in ipairs(self.oneShots) do self:deleteSource(source, true) end
    self.oneShots = {}
    local owners = {}
    for owner in pairs(self.loopOwners) do owners[#owners+1] = owner end
    for _, owner in ipairs(owners) do self:stopOwner(owner) end
    self.loopOwners = setmetatable({}, {__mode="k"})
end

addModEventListener(MudSystemWorldSounds)
