lzr_ambience = {}

-- The background/ambience music mod for Lazarr!
-- Originally this mod only contained pure ambience sounds
-- but then was switched to use music files instead.
-- The name `lzr_ambience` is kept for historic reasons,
-- To the user, the sound files here must be called "music".
-- Internally, they are called "ambiences".

local S = minetest.get_translator("lzr_ambience")

-- Gain multiplayer for all sounds (between 0.0 and 1.0)
local MASTER_GAIN = 1.0

-- Minimum gain while ambience is still active.
-- Needed because the sound will be deleted if sound_fade
-- is called with gain of 0.
local MIN_GAIN = 0.001

-- How fast the gain changes when changing the ambience
-- gain with lzr_ambience.set_gain.
local SET_GAIN_STEP = 1

local ambiences = {}
local ambience_aliases = {}

local current_singleplayer_ambience = nil -- ambience sound handler. nil if no current ambience
local current_singleplayer_ambience_id = nil -- ambience type ID (e.g. "pirates"). nil if uninitialized
local ambience_active = true -- whether to play ambience
local ambience_reduced = false -- whether volume of ambience is currently reduced to to sound effect
local ambience_reduced_timer = nil
local ambience_play_times = {}

lzr_ambience.DEFAULT_AMBIENCE = "pirates"
local REDUCED_GAIN = 0.1
local REDUCED_GAIN_STEP_DOWN = 5
local REDUCED_GAIN_STEP_UP = 0.2

local REPEAT_WRAP = 60000

lzr_ambience.reduce_ambience = function(reduce_time)
	ambience_reduced = true
	if ambience_reduced_timer then
		ambience_reduced_timer = math.max(ambience_reduced_timer, reduce_time)
	else
		ambience_reduced_timer = reduce_time
	end
	if current_singleplayer_ambience then
		local ambience = ambiences[current_singleplayer_ambience_id]
		local player_gain = lzr_ambience.get_gain()
		local gain = ambience.gain * player_gain * REDUCED_GAIN * MASTER_GAIN
		gain = math.max(MIN_GAIN, gain)
		minetest.sound_fade(current_singleplayer_ambience, REDUCED_GAIN_STEP_DOWN, gain)
	end
end

lzr_ambience.unreduce_ambience = function()
	ambience_reduced = false
	ambience_reduced_timer = nil
	if current_singleplayer_ambience then
		local ambience = ambiences[current_singleplayer_ambience_id]
		local sgain = lzr_ambience.get_gain()
		local gain = ambience.gain * sgain * MASTER_GAIN
		gain = math.max(MIN_GAIN, gain)
		minetest.sound_fade(current_singleplayer_ambience, REDUCED_GAIN_STEP_UP, gain)
	end
end

lzr_ambience.register_ambience = function(id, soundname, gain, description)
	ambiences[id] = { soundname = soundname, gain = gain, description = description }
	ambience_play_times[id] = 0
end

lzr_ambience.register_ambience_alias = function(old_name, new_name)
	ambience_aliases[old_name] = new_name
end

local registered_on_ambience_changes = {}
lzr_ambience.register_on_ambience_change = function(func)
	table.insert(registered_on_ambience_changes, func)
end
local function report_ambience_change(new_state)
	for i=1, #registered_on_ambience_changes do
		registered_on_ambience_changes[i](new_state)
	end
end

local function stop_ambience()
	if current_singleplayer_ambience then
		minetest.sound_stop(current_singleplayer_ambience)
		current_singleplayer_ambience = nil
	end
end

function lzr_ambience.ambience_exists(id)
	if ambiences[id] then
		return true
	else
		return false
	end
end

function lzr_ambience.get_ambiences()
	local list = {}
	for id,_ in pairs(ambiences) do
		table.insert(list, id)
	end
	table.sort(list, function(t1, t2)
		if t1 == "none" and t2 ~= "none" then
			return true
		elseif t1 ~= "none" and t2 == "none" then
			return false
		else
			local d1 = lzr_ambience.get_ambience_description(t1)
			local d2 = lzr_ambience.get_ambience_description(t2)
			return d1 < d2
		end
	end)
	return list
end

function lzr_ambience.get_ambience_description(id)
	if ambience_aliases[id] then
		id = ambience_aliases[id]
	end
	return ambiences[id].description
end

function lzr_ambience.get_gain()
	local gain = minetest.settings:get("lzr_ambience_gain")
	gain = tonumber(gain)
	if not gain then
		gain = 1.0
	end
	gain = math.max(0, math.min(1, gain))
	return gain
end

function lzr_ambience.set_gain(set_gain)
	set_gain = math.max(0, math.min(1, set_gain))
	minetest.settings:set("lzr_ambience_gain", tostring(set_gain))

	-- Update current ambience gain, if active
	if lzr_ambience.is_active() and current_singleplayer_ambience then
		local ambience = ambiences[current_singleplayer_ambience_id]
		local new_gain = MASTER_GAIN * ambience.gain * set_gain
		new_gain = math.max(MIN_GAIN, new_gain)
		ambience_reduced = false
		ambience_reduced_timer = nil
		minetest.sound_fade(current_singleplayer_ambience, SET_GAIN_STEP, new_gain)
	end
	minetest.log("action", "[lzr_ambience] Ambience gain set to "..set_gain)
end

function lzr_ambience.set_ambience(id, no_play)
	if ambience_aliases[id] then
		id = ambience_aliases[id]
	end
	local ambience = ambiences[id]
	if not ambience then
		minetest.log("error", "[lzr_ambience] set_ambience called with invalid ambience ID ("..tostring(id)..")")
		return false
	end
	minetest.log("info", "[lzr_ambience] Ambience set to: "..tostring(id))
	-- No-op if we already have this ambience active
	if current_singleplayer_ambience_id == id and current_singleplayer_ambience then
		return true
	end
	stop_ambience()
	if id ~= "none" and not no_play and ambience_active then
		local gain

		local sgain = lzr_ambience.get_gain()

		if ambience_reduced then
			gain = ambience.gain * sgain * REDUCED_GAIN * MASTER_GAIN
		else
			gain = ambience.gain * sgain * MASTER_GAIN
		end
		gain = math.max(MIN_GAIN, gain)
		local start_time = ambience_play_times[id]
		current_singleplayer_ambience = minetest.sound_play({name=ambience.soundname}, {gain = gain, to_player="singleplayer", loop=true, start_time=start_time})
	end
	current_singleplayer_ambience_id = id
	return true
end

function lzr_ambience.toggle_ambience(save_setting)
	if current_singleplayer_ambience then
		ambience_active = false
		stop_ambience()
		report_ambience_change(ambience_active)
		if save_setting then
			minetest.settings:set_bool("lzr_ambience", false)
		end
		return false
	else
		ambience_active = true
		lzr_ambience.set_ambience(current_singleplayer_ambience_id)
		report_ambience_change(ambience_active)
		if save_setting then
			minetest.settings:set_bool("lzr_ambience", true)
		end
		return true
	end
end

function lzr_ambience.toggle_ambience_by_player(player, save_setting)
	local new_status = lzr_ambience.toggle_ambience(save_setting)
	if new_status then
		lzr_messages.show_message(player, S("Music enabled"), 2)
		return true
	else
		lzr_messages.show_message(player, S("Music disabled"), 2)
		return false
	end
end

function lzr_ambience.is_active()
	return ambience_active
end

minetest.register_globalstep(function(dtime)
	if not ambience_active then
		return
	end
	if ambience_reduced and ambience_reduced_timer then
		ambience_reduced_timer = ambience_reduced_timer - dtime
		if ambience_reduced_timer <= 0 then
			lzr_ambience.unreduce_ambience()
		end
	end
	if current_singleplayer_ambience_id then
		ambience_play_times[current_singleplayer_ambience_id] = ambience_play_times[current_singleplayer_ambience_id] + dtime
		if ambience_play_times[current_singleplayer_ambience_id] > REPEAT_WRAP then
			ambience_play_times[current_singleplayer_ambience_id] = 0
		end
	end
end)

local setting = minetest.settings:get_bool("lzr_ambience", true)
minetest.log("info", "[lzr_ambience] setting 'lzr_ambience' loaded as "..tostring(setting))

minetest.register_on_joinplayer(function(player)
	-- Set initial ambience when player joins
	local first_ambience
	if lzr_levels.is_intro_completed() then
		-- If intro (the first level on starting the game the
		-- first time) was completed, player will spawn in ship,
		-- so we play the track for the ship
		first_ambience = lzr_ambience.DEFAULT_AMBIENCE
	else
		-- If intro was not completed, we play no ambience at first;
		-- we expect the ambience to be changed once the level
		-- has loaded. This is to prevent an awkawrd music switch
		-- after the loading phase.
		first_ambience = "none"
	end

	-- Is music enabled?
	if setting then
		ambience_active = true
		report_ambience_change(ambience_active)
		lzr_ambience.set_ambience(first_ambience)
	else
		ambience_active = false
		report_ambience_change(ambience_active)
		lzr_ambience.set_ambience(first_ambience, true)
	end
end)

minetest.register_chatcommand("music", {
	description = S("Toggle music or change music volume"),
	--~ Chat command parameters for /music command. Only translate the word "number", leave the rest intact
	params = S("[ toggle | (volume <number>) ]"),
	func = function(name, param)
		local percent = function(num)
			return math.floor(num*100)
		end
		if param == "" or param == "toggle" then
			local player = minetest.get_player_by_name(name)
			local new_status = lzr_ambience.toggle_ambience_by_player(player, true)
			if new_status then
				local vol = lzr_ambience.get_gain()
				if vol <= 0.5 then
					-- Mention volume in chat when it’s low to avoid confusion
					return true, S("Music enabled (volume: @1%).", percent(vol))
				else
					return true, S("Music enabled.")
				end
			else
				return true, S("Music disabled.")
			end
		else
			local elems = string.split(param, " ")
			if #elems == 2 and elems[1] == "volume" then
				local vol = tonumber(elems[2])
				if not vol or vol < 0 or vol > 1 then
					return false, S("Volume must be a number between 0.0 and 1.0.")
				end
				lzr_ambience.set_gain(vol)
				if lzr_ambience.is_active() then
					return true, S("Music volume set to @1%.", percent(vol))
				else
					return true, S("Music volume set to @1%. This will take effect when the music is enabled again.", percent(vol))
				end
			end
		end
		return false
	end,
})

-- Special built-in ambience that is completely silent
--~ Special entry in music list representing silence
lzr_ambience.register_ambience("none", nil, nil, S("None"))

dofile(minetest.get_modpath("lzr_ambience").."/register.lua")
