local get_node_or_nil = minetest.get_node_or_nil
local hash_node_pos = minetest.hash_node_position
local vec_add = vector.add
local floor = math.floor
local ceil = math.ceil


--implement a simple stack
--TODO: make this a proper class
local pop_to_visit
local add_to_visit
local kill_to_visit
do
	to_visit_counter = 0
	to_visit = {}
	add_to_visit = function(tab)
		counter = to_visit_counter + 1
		to_visit[counter] = tab
	end

	pop_to_visit = function()
		local ret = to_visit[counter]
		to_visit[counter] = nil
		counter = counter - 1
		return ret
	end
	kill_to_visit = function()
		to_visit = {}
		counter = 0
	end
end


local sides = {
	{x = 1, y = 0, z = 0},
	{x = -1, y = 0, z = 0},
	{x = 0, y = 1, z = 0},
	{x = 0, y = -1, z = 0},
	{x = 0, y = 0, z = 1},
	{x = 0, y = 0, z = -1},
}

local calculate_resistance
do
	local relevant_groups =
		{
			crumbly = 0.9,
			cracky = 0.9,
			snappy = 0.1,
			explody = 0.1,
			choppy = 0.5,
		}

	calculate_resistance = function(groups)
		local resistance = 0
		for groupname, weight in pairs(relevant_groups)
		do
			local g = groups[groupname]
			if g
			then
				resistance = resistance + (4 - g) * weight
			end

		end
		return resistance
	end

end

local reg_nodes = minetest.registered_nodes
local get_erosion_result

local cid_def_map = {}
local cid_air
local cid_fire
erosion.do_once_postprocessing_done(function()
	cid_air = minetest.get_content_id("air")
	cid_fire = minetest.get_content_id("fire:basic_flame")
	for name, def in pairs(reg_nodes)
	do
		cid_def_map[minetest.get_content_id(name)] =
			{
				on_blast = def.on_blast,
				water = (def.groups or {water = 0}).water or 0,
				should_fly = (def.groups or {crumbly = 0}).crumbly or 0 > 0 or nil
			}
	end
	get_erosion_result = erosion.get_erosion_result
end)



local voxel_buffer = {}
local function blast_effect(pos, power)
	minetest.add_particlespawner(
	{
		amount = 50,
		time = 0.1,
		minpos = pos,
		maxpos = pos,
		minvel = {x = -10, y = -10, z = -10},
		maxvel = {x = 10, y = 10, z = 10},
		minexptime = 0.1,
		maxexptime = 0.5,
		minsize = power,
		maxsize = power,
		texture = "gunpowder_barrels_spark.png",
		glow = 14,
	})
	--TODO: sound

	local radius = 5 - 5 / power
	if radius < 0 then radius = 0 end

	local vm = minetest.get_voxel_manip()
	local minpos = {
		x = floor(pos.x - radius),
		y = floor(pos.y - radius),
		z = floor(pos.z - radius)
		}
	local maxpos = {
		x = ceil(pos.x + radius),
		y = ceil(pos.y + radius),
		z = ceil(pos.z + radius)
		}

	local emin, emax = vm:read_from_map(minpos, maxpos)
	local area = VoxelArea:new{MinEdge = emin, MaxEdge = emax}


	local data = vm:get_data(voxel_buffer)

	local flyers = {} --stores nodes to be changed to falling

	for iz = minpos.z, maxpos.z
	do
		for iy = minpos.y, maxpos.y
		do
			for ix = minpos.x, maxpos.x
			do
				--possible optimation: cache area:index
				local position_vector = {x = ix, y = iy, z = iz}
				local dist = vector.distance(pos, position_vector)
				if dist < radius and math.random() < 0.8
				then
					local cid = data[area:index(ix, iy, iz)]
					local def = cid_def_map[cid]
					if def
					then
						--call on_blast callback
						--nodes aren't removed by anything.
						if def.on_blast
						then
							local replace = def.on_blast(position_vector,
								power / (dist ~= 0 and dist or 0.1))
						else
							replace = get_erosion_result(cid, power /
								(dist ~= 0 and dist or 0.1))
							if cid_def_map[replace or cid].should_fly
							then
								flyers[#flyers + 1] = position_vector
							end
						end

						if replace
						then
							data[area:index(ix, iy, iz)] = replace
						end

						--set fire to things
						if cid == cid_air and
							iy - 1 > emin.y and
							math.random() < 0.02
						then

							local lowcid = data[area:index(ix, iy - 1, iz)]
							local lowdef = cid_def_map[lowcid]

							if lowcid ~= cid_air and lowdef and lowdef.water == 0
							then
								data[area:index(ix, iy, iz)] = cid_fire
							end
						end
					end
				end
			end
		end
	end
	vm:set_data(data)
	vm:update_liquids()
	vm:write_to_map(true)

	for _, v in ipairs(flyers)
	do
		minetest.spawn_falling_node(v)
	end
	local objs = minetest.get_objects_inside_radius(pos, radius)
	for _, obj in ipairs(objs)
	do
		local p = obj:get_pos()
		local dist = vector.distance(pos, p)
		if dist == 0
		then
			dist = 0.01 --prevent division by zero
		end

		local upvel = power / dist
		if upvel ~= upvel --nan check
		then
			upvel = 10
		end
		if obj:is_player()
		then
			obj:add_player_velocity({x = 0, y = upvel, z = 0})
		else
			obj:setvelocity({x = 0, y = upvel, z = 0})
		end
		obj:punch(obj, 10,
		{
			damage_groups =
			{
				fleshy = (power / dist * 2)
			}
		})
	end

end


local propagatable_threshold = 0.2
local propagate = function(pos, power)
	--make a value for how confined it is
	--decrease power for low confinement

	local propagatable_num = 0
	local epicentres = {}

	local already_visited = {}

	add_to_visit({vec = pos, power = power})

	while true
	do
		local p = pop_to_visit()
		if not p
		then
			break
		end
		local hash = hash_node_pos(p.vec)
		already_visited[hash] = true

		power = p.power

		if power > 0.1 --don't propagate further if too weak
		then
			local pneighbors = {}
			local pcount = 0

			for i = 1, 6
			do
				local neighbor = vec_add(sides[i], p.vec)
				if not already_visited[hash_node_pos(neighbor)]
				then
					local node = get_node_or_nil(neighbor)
					if node
					then
						node = reg_nodes[node.name]
						local resistance = calculate_resistance(node.groups or {groups = {}})

						if resistance < propagatable_threshold
						then
							pcount = pcount + 1
							pneighbors[pcount] = neighbor
							pneighbors[pcount + 0.5] = resistance
							--TODO: take resistance into account
						end--if
					end--if
				end--if
			end--for


			power = power / (pcount + 1)
			p.power = power

			propagatable_num = propagatable_num + 1
			epicentres[propagatable_num] = p

			for i = 1, pcount, 1
			do
				local pow = power - pneighbors[i + 0.5]
				if pow < 0 then pow = 0 end
				add_to_visit({vec = pneighbors[i], power = pow})
			end--for
		end--if
	end--while



	kill_to_visit()
	for i = 1, propagatable_num
	do
		epicentre = epicentres[i]
		blast_effect(epicentre.vec, epicentre.power)
	end

end--function

local explode = function(self)
	if self.fuse_handle
	then
		minetest.sound_stop(self.fuse_handle)
	end
	minetest.sound_play(
		"gunpowder_barrels_blast",
		{
			object = self.object,
			max_hear_distance = 128,
			gain = 4,
		})

	local pos = self.object:get_pos()
	local power = 20-- + math.random() * 10
	self.object:remove()
	propagate(pos, power)
end
gunpowder_barrels.explode = explode
