-- LUALOCALS < ---------------------------------------------------------
local error, ipairs, math, minetest, pairs, piredo_api, piredo_map,
      piredo_player, string, tonumber, type, vector
    = error, ipairs, math, minetest, pairs, piredo_api, piredo_map,
      piredo_player, string, tonumber, type, vector
local math_cos, math_pi, math_random, string_format
    = math.cos, math.pi, math.random, string.format
-- LUALOCALS > ---------------------------------------------------------

local modname = minetest.get_current_modname()
local hash = minetest.hash_node_position
local ambgroup = "ambiance"

------------------------------------------------------------------------
-- Discover known sources of ambiance

local known_sources = {}

local function checkamb(pos, node)
	node = node or minetest.get_node(pos)
	local def = minetest.registered_nodes[node.name]
	local amb = def and def[ambgroup]
	if not amb then return end
	if amb.sparse then
		for _, v in pairs(known_sources) do
			if v.amb.spec == amb.spec then
				local dist = vector.distance(v.pos, pos)
				if dist > 0 and dist < amb.sparse
				then return end
			end
		end
	end
	if amb.surface then
		local apos = vector.offset(pos, 0, 1, 0)
		local anode = minetest.get_node(apos)
		local adef = minetest.registered_nodes[anode.name]
		if not (adef and adef.air_equivalent) then return end
	end
	return amb
end

local function notify(pos, node)
	local amb = checkamb(pos, node)
	if not amb then return end
	local key = hash(pos)
	if known_sources[key] then return end
	local c = {
		pos = pos,
		amb = amb,
		cycle = math_random() * 2 * amb.interval,
	}
	c.time = math_random() * c.cycle
	known_sources[key] = c
end

piredo_map.register_on_putschem(function(pos, _, roomdata)
		for _, p in ipairs(minetest.find_nodes_in_area(pos,
				vector.add(pos, roomdata.size),
				"group:" .. ambgroup)) do
			notify(p)
		end
	end)
minetest.register_abm({
		nodenames = {"group:" .. ambgroup},
		interval = 1,
		chance = 1,
		action = notify
	})
minetest.register_lbm({
		name = modname .. ":ambience",
		nodenames = {"group:" .. ambgroup},
		run_at_every_load = true,
		action = notify
	})

local oldtotal
minetest.register_globalstep(function(dtime)
		local total = 0
		local rm = {}
		for k, v in pairs(known_sources) do
			if checkamb(v.pos) then
				v.time = (v.time or 0) + dtime
				if (not v.cycle) or (v.time > v.cycle) then
					v.time = 0
					v.cycle = math_random() * 2 * v.amb.interval
				end
				total = total + 1
			else
				rm[#rm + 1] = k
			end
		end
		for i = 1, #rm do known_sources[rm[i]] = nil end
		if total ~= oldtotal then
			oldtotal = total
			minetest.log("info", string_format("%s sources: %d",
					ambgroup, total))
		end
	end)

------------------------------------------------------------------------
-- Manage ambiance for players

local pdata = piredo_player.create_player_data()

local function calcgain(posa, posb, gain, ended)
	if ended then return 0.0001 end
	local dist = vector.distance(posa, posb)
	if dist > 8 then return 0.0001 end
	if dist < 4 then return (gain or 1) / 5 + 0.0001 end
	local ramp = math_cos(math_pi * (dist - 4) / 4) / 2 + 0.5
	local s = tonumber(minetest.settings:get("piranesi_amb_terrain")) or 1
	return s * (gain or 1) / 5 * ramp + 0.0001
end

local bird_startup_wait = true
minetest.after(5, function() bird_startup_wait = nil end)
local function birdcheck(pos)
	if bird_startup_wait then return end
	local t = minetest.get_timeofday()
	local p = 1
	if t < 1/6 or t > 5/6 then
		p = 0
	elseif t < 11/48 then
		p = (t - 1/6) * 16
	elseif t > 37/48 then
		p = (5/6 - t) * 16
	end
	local r = math_random()
	if r < p / 20 then
		return minetest.sound_play("piredo_ambiance_bird",
			{pos = pos}, true)
	elseif r < (1 - p) / 5 then
		return minetest.sound_play("piredo_ambiance_cricket",
			{pos = pos}, true)
	end
end

piredo_player.register_playerstep(function(player, dtime)
		local ppos = player:get_pos()
		ppos.y = ppos.y + player:get_properties().eye_height
		local data = pdata(player)
		local ended = piredo_player:is_endcredits()
		for k, v in pairs(known_sources) do
			local d = data[k]
			if d and v.time == 0 then
				minetest.sound_fade(d.id, 0.1, 0)
				d = nil
			end
			if (not d) or (v.time == 0 and not v.amb.loop) then
				d = {gain = calcgain(v.pos, ppos, v.amb.gain, ended)}
				local param = {}
				for sk, sv in pairs(v.amb.spec) do param[sk] = sv end
				param.pos = v.pos
				param.gain = d.gain
				param.max_hear_distance = 128
				param.loop = v.amb.loop
				param.start_time = v.time
				d.id = minetest.sound_play(param.name, param)
				data[k] = d
			else
				local gain = calcgain(v.pos, ppos, v.amb.gain, ended)
				if gain ~= d.gain then
					minetest.sound_fade(d.id, 0.1, gain)
					d.gain = gain
				end
			end
			if v.amb.birds and not ended then
				d.birdtime = (d.birdtime or (math_random() * 10)) - dtime
				if d.birdtime < 0 then
					d.birdtime = 11 - math_random() * 2
					if v.pos.y > ppos.y then birdcheck(v.pos) end
				end
			end
		end
		local rm = {}
		for k, v in pairs(data) do
			if not known_sources[k] then
				minetest.sound_stop(v.id)
				rm[#rm + 1] = k
			end
		end
		for i = 1, #rm do
			data[rm[i]] = nil
		end
	end)

local function setambiance(nodename, data)
	local def = minetest.registered_nodes[nodename]
	if not def then error(nodename .. " not found") end
	data.spec = data.spec or def.sounds and (def.sounds.footstep
		or def.sounds.dig or def.sounds.dug)
	if type(data.spec) == "string" then
		data.spec = {name = data.spec}
	end
	if not data.spec then error("no ambient sound") end
	piredo_api.add_groups(nodename, ambgroup)
	return minetest.override_item(nodename, {[ambgroup] = data})
end

setambiance("piredo_terrain:default__water_source", {
		sparse = 2,
		interval = 15,
		gain = 0.25,
		pitch = 2,
		surface = true,
	})
setambiance("piredo_terrain:default__water_flowing", {
		sparse = 2,
		interval = 5,
		gain = 0.15,
		loop = true,
	})
setambiance("piredo_terrain:default__lava_source", {
		interval = 10,
		surface = true,
		spec = "xdecor_boiling_water",
	})
setambiance("piredo_terrain:default__lava_flowing", {
		interval = 10,
		gain = 0.25,
		loop = true,
		spec = "xdecor_boiling_water",
	})

setambiance("piredo_terrain:piranesi__cauldron", {
		interval = 1,
		spec = "xdecor_boiling_water",
	})
setambiance("piredo_terrain:piranesi__cauldron_purple", {
		interval = 1,
		spec = "xdecor_boiling_water",
	})
setambiance("piredo_terrain:piranesi__cauldron_black", {
		interval = 1,
		spec = "xdecor_boiling_water",
	})
setambiance("piredo_terrain:piranesi__machine_gear_1_a", {
		interval = 5,
		spec = "piredo_puzzles_sztest_gears",
	})
setambiance("piredo_terrain:piranesi__machine_gear_2_a", {
		interval = 5,
		spec = "piredo_puzzles_sztest_gears",
	})
setambiance("piredo_terrain:fire__basic_flame", {
		interval = 10,
		spec = "fire_small",
		gain = 0.25,
		loop = true,
	})
setambiance("piredo_terrain:fire__permanent_flame", {
		interval = 10,
		spec = "fire_small",
		gain = 0.25,
		loop = true,
	})

for _, n in ipairs({"aspen_leaves", "leaves"}) do
	setambiance("piredo_terrain:default__" .. n, {
			sparse = 5,
			surface = true,
			interval = 10,
			spec = "piredo_ambiance_leaves",
			gain = 0.5,
			loop = true,
			birds = true,
		})
end
