-- Global namespace for functions

mcl_fire = {}

local modname = minetest.get_current_modname()
local modpath = minetest.get_modpath(modname)
local S = minetest.get_translator(modname)

local has_mcl_portals = minetest.get_modpath("mcl_portals")

local set_node = minetest.set_node
local get_node = minetest.get_node
local add_node = minetest.add_node
local remove_node = minetest.remove_node
local swap_node = minetest.swap_node
local get_node_or_nil = minetest.get_node_or_nil

local find_nodes_in_area = minetest.find_nodes_in_area
local find_node_near = minetest.find_node_near
local get_item_group = minetest.get_item_group

local get_connected_players = minetest.get_connected_players

local vector = vector
local math = math

-- inverse pyramid pattern above lava source, floor 1 of 2:
local lava_fire=
{
	{ x =-1, y = 1, z =-1},
	{ x =-1, y = 1, z = 0},
	{ x =-1, y = 1, z = 1},
	{ x = 0, y = 1, z =-1},
	{ x = 0, y = 1, z = 0},
	{ x = 0, y = 1, z = 1},
	{ x = 1, y = 1, z =-1},
	{ x = 1, y = 1, z = 0},
	{ x = 1, y = 1, z = 1}
}
local alldirs=
{
	{ 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 smoke_pdef = {
	amount = 0.009,
	maxexptime = 4.0,
	minvel = { x = -0.1, y = 0.3, z = -0.1 },
	maxvel = { x = 0.1, y = 1.6, z = 0.1 },
	minsize = 4.0,
	maxsize = 4.5,
	minrelpos = { x = -0.45, y = -0.45, z = -0.45 },
	maxrelpos = { x = 0.45, y = 0.45, z = 0.45 },
}

--
-- Items
--

-- Flame nodes

-- Fire settings

-- When enabled, fire destroys other blocks.
local fire_enabled = minetest.settings:get_bool("enable_fire", true)

-- Enable sound
local flame_sound = minetest.settings:get_bool("flame_sound", true)

-- Help texts
local fire_help, eternal_fire_help
if fire_enabled then
	fire_help = S("Fire is a damaging and destructive but short-lived kind of block. It will destroy and spread towards near flammable blocks, but fire will disappear when there is nothing to burn left. It will be extinguished by nearby water and rain. Fire can be destroyed safely by punching it, but it is hurtful if you stand directly in it. If a fire is started above netherrack or a magma block, it will immediately turn into an eternal fire.")
else
	fire_help = S("Fire is a damaging but non-destructive short-lived kind of block. It will disappear when there is no flammable block around. Fire does not destroy blocks, at least not in this world. It will be extinguished by nearby water and rain. Fire can be destroyed safely by punching it, but it is hurtful if you stand directly in it. If a fire is started above netherrack or a magma block, it will immediately turn into an eternal fire.")
end

if fire_enabled then
	eternal_fire_help = S("Eternal fire is a damaging block that might create more fire. It will create fire around it when flammable blocks are nearby. Eternal fire can be extinguished by punches and nearby water blocks. Other than (normal) fire, eternal fire does not get extinguished on its own and also continues to burn under rain. Punching eternal fire is safe, but it hurts if you stand inside.")
else
	eternal_fire_help = S("Eternal fire is a damaging block. Eternal fire can be extinguished by punches and nearby water blocks. Other than (normal) fire, eternal fire does not get extinguished on its own and also continues to burn under rain. Punching eternal fire is safe, but it hurts if you stand inside.")
end

local function fire_timer(pos)
	minetest.get_node_timer(pos):start(math.random(3, 7))
end

local function spawn_fire(pos, age)
	set_node(pos, {name="mcl_fire:fire", param2 = age})
	minetest.check_single_for_falling({x=pos.x, y=pos.y+1, z=pos.z})
end

minetest.register_node("mcl_fire:fire", {
	description = S("Fire"),
	_doc_items_longdesc = fire_help,
	drawtype = "firelike",
	tiles = {
		{
			name = "fire_basic_flame_animated.png",
			animation = {
				type = "vertical_frames",
				aspect_w = 16,
				aspect_h = 16,
				length = 1
			},
		},
	},
	inventory_image = "fire_basic_flame.png",
	paramtype = "light",
	light_source = minetest.LIGHT_MAX,
	walkable = false,
	buildable_to = true,
	sunlight_propagates = true,
	damage_per_second = 1,
	groups = {fire = 1, dig_immediate = 3, not_in_creative_inventory = 1, dig_by_piston=1, destroys_items=1, set_on_fire=8},
	floodable = true,
	on_flood = function(pos, oldnode, newnode)
		if get_item_group(newnode.name, "water") ~= 0 then
			minetest.sound_play("fire_extinguish_flame", {pos = pos, gain = 0.25, max_hear_distance = 16}, true)
		end
	end,
	on_timer = function(pos)
		local node = get_node(pos)
		-- Age is a number from 0 to 15 and is increased every timer step.
		-- "old" fire is more likely to be extinguished
		local age = node.param2
		local flammables = find_nodes_in_area({x=pos.x-1, y=pos.y-1, z=pos.z-1}, {x=pos.x+1, y=pos.y+4, z=pos.z+1}, {"group:flammable"})
		local below = get_node({x=pos.x, y=pos.z-1, z=pos.z})
		local below_is_flammable = get_item_group(below.name, "flammable") > 0
		-- Extinguish fire
		if (not fire_enabled) and (math.random(1,3) == 1) then
			remove_node(pos)
			return
		end
		if age == 15 and not below_is_flammable then
			remove_node(pos)
			return
		elseif age > 3 and #flammables == 0 and not below_is_flammable and math.random(1,4) == 1 then
			remove_node(pos)
			return
		end
		local age_add = 1
		-- If fire spread is disabled, we have to skip the "destructive" code
		if (not fire_enabled) then
			if age + age_add <= 15 then
				node.param2 = age + age_add
				set_node(pos, node)
			end
			-- Restart timer
			fire_timer(pos)
			return
		end
		-- Spawn fire to nearby flammable nodes
		local is_next_to_flammable = find_node_near(pos, 2, {"group:flammable"}) ~= nil
		if is_next_to_flammable and math.random(1,2) == 1 then
			-- The fire we spawn copies the age of this fire.
			-- This prevents fire from spreading infinitely far as the fire fire dies off
			-- quicker the further it has spreaded.
			local age_next = math.min(15, age + math.random(0, 1))
			-- Select random type of fire spread
			local burntype = math.random(1,2)
			if burntype == 1 then
				-- Spawn fire in air
				local nodes = find_nodes_in_area({x=pos.x-1, y=pos.y-1, z=pos.z-1}, {x=pos.x+1, y=pos.y+4, z=pos.z+1}, {"air"})
				while #nodes > 0 do
					local r = math.random(1, #nodes)
					if find_node_near(nodes[r], 1, {"group:flammable"}) then
						spawn_fire(nodes[r], age_next)
						break
					else
						table.remove(nodes, r)
					end
				end
			else
				-- Burn flammable block
				local nodes = find_nodes_in_area({x=pos.x-1, y=pos.y-1, z=pos.z-1}, {x=pos.x+1, y=pos.y+4, z=pos.z+1}, {"group:flammable"})
				if #nodes > 0 then
					local r = math.random(1, #nodes)
					local nn = get_node(nodes[r]).name
					local ndef = minetest.registered_nodes[nn]
					local fgroup = get_item_group(nn, "flammable")
					if ndef and ndef._on_burn then
						ndef._on_burn(nodes[r])
					elseif fgroup ~= -1 then
						spawn_fire(nodes[r], age_next)
					end
				end
			end
		end
		-- Regular age increase
		if age + age_add <= 15 then
			node.param2 = age + age_add
			set_node(pos, node)
		end
		-- Restart timer
		fire_timer(pos)
	end,
	drop = "",
	sounds = {},
	-- Turn into eternal fire on special blocks, light Nether portal (if possible), start burning timer
	on_construct = function(pos)
		local bpos = {x=pos.x, y=pos.y-1, z=pos.z}
		local under = get_node(bpos).name

		local dim = mcl_worlds.pos_to_dimension(bpos)
		if under == "mcl_nether:magma" or under == "mcl_nether:netherrack" or (under == "mcl_core:bedrock" and dim == "end") then
			swap_node(pos, {name = "mcl_fire:eternal_fire"})
		end

		if has_mcl_portals then
			mcl_portals.light_nether_portal(pos)
		end

		fire_timer(pos)
		mcl_particles.spawn_smoke(pos, "fire", smoke_pdef)
	end,
	on_destruct = function(pos)
		mcl_particles.delete_node_particlespawners(pos)
	end,
	_mcl_blast_resistance = 0,
})

minetest.register_node("mcl_fire:eternal_fire", {
	description = S("Eternal Fire"),
	_doc_items_longdesc = eternal_fire_help,
	drawtype = "firelike",
	tiles = {
		{
			name = "fire_basic_flame_animated.png",
			animation = {
				type = "vertical_frames",
				aspect_w = 16,
				aspect_h = 16,
				length = 1
			},
		},
	},
	inventory_image = "fire_basic_flame.png",
	paramtype = "light",
	light_source = minetest.LIGHT_MAX,
	walkable = false,
	buildable_to = true,
	sunlight_propagates = true,
	damage_per_second = 1,
	groups = {fire = 1, dig_immediate = 3, not_in_creative_inventory = 1, dig_by_piston = 1, destroys_items = 1, set_on_fire=8},
	floodable = true,
	on_flood = function(pos, oldnode, newnode)
		if get_item_group(newnode.name, "water") ~= 0 then
			minetest.sound_play("fire_extinguish_flame", {pos = pos, gain = 0.25, max_hear_distance = 16}, true)
		end
	end,
	on_timer = function(pos)
		if fire_enabled then
			local airs = find_nodes_in_area({x=pos.x-1, y=pos.y-1, z=pos.z-1}, {x=pos.x+1, y=pos.y+4, z=pos.z+1}, {"air"})
			while #airs > 0 do
				local r = math.random(1, #airs)
				if find_node_near(airs[r], 1, {"group:flammable"}) then
					local node = get_node(airs[r])
					local age = node.param2
					local age_next = math.min(15, age + math.random(0, 1))
					spawn_fire(airs[r], age_next)
					break
				else
					table.remove(airs, r)
				end
			end
		end
		-- Restart timer
		fire_timer(pos)
	end,
	-- Start burning timer and light Nether portal (if possible)
	on_construct = function(pos)
		fire_timer(pos)

		if has_mcl_portals then --Calling directly minetest.get_modpath consumes 4x more compute time
			mcl_portals.light_nether_portal(pos)
		end
		mcl_particles.spawn_smoke(pos, "fire", smoke_pdef)
	end,
	on_destruct = function(pos)
		mcl_particles.delete_node_particlespawners(pos)
	end,
	sounds = {},
	drop = "",
	_mcl_blast_resistance = 0,
})

--
-- Sound
--

if flame_sound then

	local handles = {}
	local timer = 0

	-- Parameters

	local radius = 8 -- Flame node search radius around player
	local cycle = 3 -- Cycle time for sound updates

	-- Update sound for player

	function mcl_fire.update_player_sound(player)
		local player_name = player:get_player_name()
		-- Search for flame nodes in radius around player
		local ppos = player:get_pos()
		local areamin = vector.subtract(ppos, radius)
		local areamax = vector.add(ppos, radius)
		local fpos, num = find_nodes_in_area(
			areamin,
			areamax,
			{"mcl_fire:fire", "mcl_fire:eternal_fire"}
		)
		-- Total number of flames in radius
		local flames = (num["mcl_fire:fire"] or 0) +
			(num["mcl_fire:eternal_fire"] or 0)
		-- Stop previous sound
		if handles[player_name] then
			minetest.sound_fade(handles[player_name], -0.4, 0.0)
			handles[player_name] = nil
		end
		-- If flames
		if flames > 0 then
			-- Find centre of flame positions
			local fposmid = fpos[1]
			-- If more than 1 flame
			if #fpos > 1 then
				local fposmin = areamax
				local fposmax = areamin
				for i = 1, #fpos do
					local fposi = fpos[i]
					if fposi.x > fposmax.x then
						fposmax.x = fposi.x
					end
					if fposi.y > fposmax.y then
						fposmax.y = fposi.y
					end
					if fposi.z > fposmax.z then
						fposmax.z = fposi.z
					end
					if fposi.x < fposmin.x then
						fposmin.x = fposi.x
					end
					if fposi.y < fposmin.y then
						fposmin.y = fposi.y
					end
					if fposi.z < fposmin.z then
						fposmin.z = fposi.z
					end
				end
				fposmid = vector.divide(vector.add(fposmin, fposmax), 2)
			end
			-- Play sound
			local handle = minetest.sound_play(
				"fire_fire",
				{
					pos = fposmid,
					to_player = player_name,
					gain = math.min(0.06 * (1 + flames * 0.125), 0.18),
					max_hear_distance = 32,
					loop = true, -- In case of lag
				}
			)
			-- Store sound handle for this player
			if handle then
				handles[player_name] = handle
			end
		end
	end

	-- Cycle for updating players sounds

	minetest.register_globalstep(function(dtime)
		timer = timer + dtime
		if timer < cycle then
			return
		end

		timer = 0
		local players = get_connected_players()
		for n = 1, #players do
			mcl_fire.update_player_sound(players[n])
		end
	end)

	-- Stop sound and clear handle on player leave

	minetest.register_on_leaveplayer(function(player)
		local player_name = player:get_player_name()
		if handles[player_name] then
			minetest.sound_stop(handles[player_name])
			handles[player_name] = nil
		end
	end)
end


--
-- ABMs
--

-- Extinguish all flames quickly with water and such

minetest.register_abm({
	label = "Extinguish fire",
	nodenames = {"mcl_fire:fire", "mcl_fire:eternal_fire"},
	neighbors = {"group:puts_out_fire"},
	interval = 3,
	chance = 1,
	catch_up = false,
	action = function(pos, node, active_object_count, active_object_count_wider)
		remove_node(pos)
		minetest.sound_play("fire_extinguish_flame",
			{pos = pos, max_hear_distance = 16, gain = 0.15}, true)
	end,
})


-- Enable the following ABMs according to 'enable fire' setting

local function has_flammable(pos)
	local npos, node
	for n, v in ipairs(alldirs) do
		npos = vector.add(pos, v)
		node = get_node_or_nil(npos)
		if node and node.name and get_item_group(node.name, "flammable") ~= 0 then
			return npos
		end
	end
	return false
end

if not fire_enabled then

	-- Occasionally remove fire if fire disabled
	-- NOTE: Fire is normally extinguished in timer function
	minetest.register_abm({
		label = "Remove disabled fire",
		nodenames = {"mcl_fire:fire"},
		interval = 10,
		chance = 10,
		catch_up = false,
		action = remove_node,
	})

else -- Fire enabled

	-- Set fire to air nodes
	minetest.register_abm({
		label = "Ignite fire by lava",
		nodenames = {"group:lava"},
		neighbors = {"air"},
		interval = 7,
		chance = 3,
		catch_up = false,
		action = function(pos)
			local i, dir, target, node, i2, f
			i = math.random(1,9)
			dir = lava_fire[i]
			target = {x=pos.x+dir.x, y=pos.y+dir.y, z=pos.z+dir.z}
			node = get_node(target)
			if not node or node.name ~= "air" then
				i = ((i + math.random(0,7)) % 9) + 1
				dir = lava_fire[i]
				target = {x=pos.x+dir.x, y=pos.y+dir.y, z=pos.z+dir.z}
				node = get_node(target)
				if not node or node.name ~= "air" then
					return
				end
			end
			i2 = math.random(1,15)
			if i2 < 10 then
				local dir2, target2, node2
				dir2 = lava_fire[i2]
				target2 = {x=target.x+dir2.x, y=target.y+dir2.y, z=target.z+dir2.z}
				node2 = get_node(target2)
				if node2 and node2.name == "air" then
					f = has_flammable(target2)
					if f then
						minetest.after(1, spawn_fire, {x=target2.x, y=target2.y, z=target2.z})
						minetest.add_particle({
							pos = vector.new({x=pos.x, y=pos.y+0.5, z=pos.z}),
							velocity={x=f.x-pos.x, y=math.max(f.y-pos.y,0.7), z=f.z-pos.z},
							expirationtime=1, size=1.5, collisiondetection=false,
							glow=minetest.LIGHT_MAX, texture="mcl_particles_flame.png"
						})
						return
					end
				end
			end
			f = has_flammable(target)
			if f then
				minetest.after(1, spawn_fire, {x=target.x, y=target.y, z=target.z})
				minetest.add_particle({
					pos = vector.new({x=pos.x, y=pos.y+0.5, z=pos.z}),
					velocity={x=f.x-pos.x, y=math.max(f.y-pos.y,0.25), z=f.z-pos.z},
					expirationtime=1, size=1, collisiondetection=false,
					glow=minetest.LIGHT_MAX, texture="mcl_particles_flame.png"
				})
			end
		end,
	})

end

-- Set pointed_thing on (normal) fire.
-- * pointed_thing: Pointed thing to ignite
-- * player: Player who sets fire or nil if nobody
-- * allow_on_fire: If false, can't ignite fire on fire (default: true)
function mcl_fire.set_fire(pointed_thing, player, allow_on_fire)
	local pname
	if player == nil then
		pname = ""
	else
		pname = player:get_player_name()
	end
	local n = get_node(pointed_thing.above)
	local nu = get_node(pointed_thing.under)
	if allow_on_fire == false and get_item_group(nu.name, "fire") ~= 0 then
		return
	end
	if minetest.is_protected(pointed_thing.above, pname) then
		minetest.record_protection_violation(pointed_thing.above, pname)
		return
	end
	if n.name == "air" then
		add_node(pointed_thing.above, {name="mcl_fire:fire"})
	end
end

minetest.register_lbm({
	label = "Smoke particles from fire",
	name = "mcl_fire:smoke",
	nodenames = {"group:fire"},
	run_at_every_load = true,
	action = function(pos, node)
		mcl_particles.spawn_smoke(pos, "fire", smoke_pdef)
	end,
})

minetest.register_alias("mcl_fire:basic_flame", "mcl_fire:fire")
minetest.register_alias("fire:basic_flame", "mcl_fire:fire")
minetest.register_alias("fire:permanent_flame", "mcl_fire:eternal_fire")

dofile(modpath.."/flint_and_steel.lua")
dofile(modpath.."/fire_charge.lua")
