-- TODO: Performance might suffer if we keep querying for each position. Besides, the difference
-- between neighbouring nodes is very little.
-- Either this can be solved here or in the wind lib. We can create bigger blocks of nodes that
-- share the same windspeed. The challenge becomes getting previously computed values. A cache hit
-- can happen in a certain time window or when it is near a previously query-ed node (vector_mod).
-- The hope is that a table lookup is way cheaper then doing several queries.
local round_robin = luanti_utils.dofile("round_robin.lua")
local on_server_idle = luanti_utils.dofile("on_server_idle.lua")
local random_each = luanti_utils.dofile("random_each.lua")
local vector_mod = luanti_utils.dofile("vector_mod.lua")
local debug = luanti_utils.dofile("debug.lua")

-- TODO: figure out how to spawn group:snowy since it uses nodeboxes which results in particles being
-- spawned above and high. Actually looks fine. Just don't spawn particles if it isn't windy enough.
-- Then spawn many and it will be too chaotic to see that they are spawning above the nodebox.
SPAWN_NODENAMES =
	{ "group:leaves", "group:soil", "group:sand", "group:water", "group:snowy", "group:grass", "group:flora" }
SPAWN_RADIUS = 32
SPAWN_RATIO = 0.4

PARTICLES_PER_MS_WINDSPEED = 3
SPAWN_INTERVAL = 5

RECOMPUTE_DISTANCE = SPAWN_RADIUS / 3

-- TODO: Check if direction changes downstream and add jitter towards that direction.
-- Calc the jitter to have it turn the wind it is going to.
-- local jitter = {min = {x=-6,y=-6,z=-6}, max ={x=6,y=6,z=6}, bias = 0}
local gravity = vector.new(0, -0.7, 0)
local up = vector.new(0, 1, 0)

local function add_particle_spawner(lpos, player)
	-- wind-based velocity and acceleration
	local w = breasy.get_occluded_wind(vector.add(lpos, up), true)
	local ws = vector.length(w) -- do something with wind speed.

	-- Important to drop the packets sent to clients by a lot.
	if ws <= 5 then
		return
	end

	local vel = w -- vector.add(w, vector.new(0, 0.2, 0))
	local acc = gravity

	-- spawn on the top always.
	local posmin = vector.offset(lpos, -0.5, 0.55, -0.5)
	local posmax = vector.offset(lpos, 0.5, 0.55, 0.5)
	local node_name = core.get_node(lpos).name

	-- This will cause the particle to turn into the wind where it is traveling to.
	-- Should give a very cool effect.
	local jitter = breasy.get_wind(vector.add(lpos, w))

	jitter = { min = jitter, max = table.copy(jitter) }

	jitter.max.y = math.random(ws / 2)
	jitter.min.y = math.random(ws / 3)

	debug("Spawning particle spawner", player:get_player_name())
	core.add_particlespawner({
		-- bigger particles at higher speeds.
		size = math.random(ws) / 20,
		-- Should randomize
		amount = ws * PARTICLES_PER_MS_WINDSPEED,
		alpha = 0.1,

		-- will live twice as long to prevent weird cut-off
		time = SPAWN_INTERVAL * 2,
		node = { name = node_name },
		playername = player:get_player_name(),
		exptime = SPAWN_INTERVAL,
		collisiondetection = true,
		collision_removal = true,
		vertical = true,
		minpos = posmin,
		maxpos = posmax,
		pos = { min = posmin, max = posmax },
		jitter = jitter,
		vel = vel,
		acc = acc,
	})
end

-- TODO: move to luanti_utils.
-- Write a better cache helper.
-- with_cache(function(cached_returns, ...) end)
-- Might not be an intuitive api since the cache has to be unpacked
-- and values are fetched using index...
--
local function recompute_when(has_changed, expensive_fn)
	local prev
	local cache

	return function(...)
		local changed, next = has_changed(prev, ...)

		if changed then
			prev = next
			cache = expensive_fn(...)
		end

		return cache
	end
end

-- The player might stay in the area and not move enough to trigger recalc.
-- In that case we need to derive if the environment has changed.
-- One way to do this is by looking at player interactions.
-- It is a bit of a shame that this is necessary just to enable this feature.
--
-- core.register_on_placenode
-- core.register_on_dignode
local function moved_enough(prev, player)
	-- By how much to multiply?
	local vel = player:get_velocity()
	vel.y = 0
	local pos = vector.add(player:get_pos(), vector.multiply(vel, SPAWN_INTERVAL / 2))
	local mod_pos = vector_mod(pos, RECOMPUTE_DISTANCE)

	if prev == nil then
		return true, mod_pos
	end

	return not vector.equals(prev, mod_pos), mod_pos
end

local find_spawn_positions = recompute_when(moved_enough, function(player)
	debug("Do expensive find nodes")
	local pos = player:get_pos()
	local p1 = vector.add(pos, -SPAWN_RADIUS)
	local p2 = vector.add(pos, SPAWN_RADIUS)
	return core.find_nodes_in_area_under_air(p1, p2, SPAWN_NODENAMES)
end)

local function on_player(next, player, players)
	local pos = player:get_pos()
	local positions = find_spawn_positions(player)

	local function on_position(node_pos)
		on_server_idle.run(function()
			add_particle_spawner(node_pos, player)
		end)
	end

	random_each(positions, on_position, SPAWN_RATIO)

	core.after(SPAWN_INTERVAL / #players / 3, function()
		next()
	end)
end

local rr = round_robin(core.get_connected_players, on_player)

local function on_joinplayer()
	rr:start()
end

core.register_on_joinplayer(on_joinplayer)
