--[[
Node Damage and Repair System
Copyright (C) 2020 Noodlemire

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
--]]

--mod-specific global storage
node_damage = {}

--Get the node_damage version of an item's name.
--In the original name, the comma is gsubbed into an underscore in order to comply with naming standards.
local function get_node_damage_name(name, i)
	return "node_damage:"..(name:gsub(":", "_")).."_"..i
end

--Check if any particular node is valid for crackification.
local function node_is_valid(name)
	--Get the existing definition for this item.
	local def = minetest.registered_items[name]

	--Return true if all of the following is true:
	--The definition exists
	--This item is not something that would immediately be dug up anyways
	--This item isn't already cracked
	--This item is a node
	--This item doesn't originate from this mod.
	--This item can be destroyed
	--This item is visible
	return (def and minetest.get_item_group(name, "dig_immediate") == 0 
		and minetest.get_item_group(name, "node_damage") == 0 and def.type == "node" and def.mod_origin ~= "node_damage" 
		and def.diggable and def.drawtype ~= "airlike")
end

--Once every mod has loaded and therefore every node has been registered...
minetest.register_on_mods_loaded(function()
	local queue = {}

	--For each registered item...
	for name, def in pairs(minetest.registered_nodes) do
		--If there's a valid node...
		if node_is_valid(name) then
			--Start by overriding the valid node to let it know that it has a damaged variation.
			local ndef = {node_damaged_name = get_node_damage_name(name, 1)}
			minetest.override_item(name, ndef)

			--For each of the cracked stages...
			for i = 1, 3 do
				--Copy the base node's definition and remove information from the copy that can't be overridden.
				def = table.copy(minetest.registered_items[name])
				--def.name = get_node_damage_name(name, i)
				--def.mod_origin = "node_damage"

				--Give it the not_in_creative_inventory and node_damage groups
				--The exact number of node_damage can be used to know how damaged a node is.
				def.groups = def.groups or {}
				def.groups.not_in_creative_inventory = 1
				def.groups.node_damage = i

				--If the node doesn't already define drops, make the cracked nodes only drop their fully repaired version.
				if not def.drop then
					def.drop = name
				end

				--If the damage stage is 1, repairing it gives the base node.
				if i == 1 then
					def.node_repaired_name = name
				--Otherwise, it gives the damage stage one less than this one.
				else
					def.node_repaired_name = get_node_damage_name(name, i - 1)
				end

				--If the damage stage isn't the highest, 3, damaging it gives the next stage of damage.
				if i ~= 3 then
					def.node_damaged_name = get_node_damage_name(name, i + 1)
				else
					--Stage 3 of damage doesn't place a new node when damage. They're just destroyed.
					def.node_damaged_name = nil
				end

				--If this node defines textures...
				if def.tiles then
					--For each tile...
					for k, tile in pairs(def.tiles) do
						--If the tile is just a string, overlay cracks onto the tile
						if type(tile) == "string" then
							def.tiles[k] = tile.."^node_damage_"..i..".png"
						--If there's a name field, overlay cracks onto the name
						elseif def.tiles[k].name then
							def.tiles[k].name = tile.name.."^node_damage_"..i..".png"
						--If there's an image field (deprecated), overlay cracks onto the image
						elseif def.tiles[k].image then
							def.tiles[k].image = tile.image.."^node_damage_"..i..".png"
						--Otherwise, relay an error message.
						else
							minetest.log("error: Could not overlay "..dump(tile))
						end
					end
				else
					--Otherwise, relay an error message.
					minetest.log(name.." has no tiles.")
					break
				end

				--Add the new node variation to a queue to be registered later.
				--Trying to add new nodes while looping through their table can mess things up and cause some nodes to get skipped.
				queue[":"..get_node_damage_name(name, i)] = def
			end
		end
	end

	--Finally, forcefully register each new node while skipping the mod name prefix check,
	--in order for it to work after all mods have been loaded.
	for name, def in pairs(queue) do
		minetest.register_node(name, def)
	end
end)



--A function to damage a given node, as long as that's possible.
--pos: The location of the node to damage.
--node: Optional; the node itself. It there's already data, you can pass it along to save time, but this function can grab the data itself.
--digger: Optional; A person (ObjectRef) who is attacking this node. If its destroyed, that person will pick it up.
--num: Optional, defaults to 1; The amount of times to damage this node. 3 will destroy any node_damage node.
--Returns a boolean indicating success.
function node_damage.damage(pos, node, digger, num)
	--If the node itself wasn't provided, grab it from the provided position
	if not node then
		node = minetest.get_node(pos)
	end

	--By default, assume that there is no player directly causing the damage.
	local pname = ""

	--If there is a player, get that player's name instead.
	if digger then
		pname = digger:get_player_name()
	end

	--If this node is protected, fail.
	if minetest.is_protected(pos, pname) then
		return false
	end

	--Get the node's definition
	local def = minetest.registered_items[node.name]

	--If the def has no definition or specifies that it can't be dug right now, fail.
	if not def or (def.can_dig and not def.can_dig()) then
		return false
	end

	--If it defines a next stage of damage, swap to that next stage.
	if def.node_damaged_name and minetest.registered_items[def.node_damaged_name] then
		minetest.swap_node(pos, {name = def.node_damaged_name, param1 = node.param1, param2 = node.param2})
	--Otherwise, if it doesn't because it's in the last stage of damage or able to be immediately dug, destroy the node.
	elseif def.groups.node_damage == 3 or def.groups.dig_immediate > 0 then
		--If a digger was provided, use node_dig to give the node to them.
		if digger then
			minetest.node_dig(pos, node, digger)
		--Otherwise, use the standard dig_node. This leaves the drops on the ground.
		else
			minetest.dig_node(pos, node)
		end

		--If the node was dug up and left nothing behind, there's no need to apply more damage.
		if minetest.get_node(pos).name == "air" then
			return true
		end
	else
		--If it's not possible to damage this node, fail.
		return false
	end

	--If num was provided and it was more than 1...
	if num and num > 1 then
		--Call damage again, but with 1 less num.
		return node_damage.damage(pos, node, digger, num - 1)
	end

	--Getting to the end of the function is only possible upon success.
	return true
end

--The opposite of damage. Removes damage from a node, as long as its possible.
--pos: The location of the node to repair.
--node: Optional; the node itself. It there's already data, you can pass it along to save time, but this function can grab the data itself.
--fixer: Optional; A person (ObjectRef) who is repairing this node. Used to determine protection permission.
--num: Optional, defaults to 1; The amount of times to repair this node. 3 will repair any node that still exists.
--Returns a boolean indicating success.
function node_damage.repair(pos, node, fixer, num)
	--If the node itself wasn't provided, grab it from the provided position
	if not node then
		node = minetest.get_node(pos)
	end

	--By default, assume that there is no player directly causing the repair.
	local pname = ""

	--If there is a player, get that player's name instead.
	if fixer then
		pname = fixer:get_player_name()
	end

	--If this node is protected, fail.
	if minetest.is_protected(pos, pname) then
		return false
	end

	--Get the node's definition
	local def = minetest.registered_items[node.name]

	--If there is no definition for this node, fail.
	if not def then
		return false
	end

	--If the definition has the name of a less damaged node to switch to, swap the node.
	if def.node_repaired_name and minetest.registered_items[def.node_repaired_name] then
		minetest.swap_node(pos, {name = def.node_repaired_name, param1 = node.param1, param2 = node.param2})
	else
		--If it's not possible to repair this node, fail.
		return false
	end

	--If num was provided and it was more than 1...
	if num and num > 1 then
		--Call repair again, but with 1 less num.
		return node_damage.repair(pos, node, fixer, num - 1)
	end

	--Getting to the end of the function is only possible upon success.
	return true
end



--Register a tool that can test both the damage and repair functions.
minetest.register_tool("node_damage:test_hammer", {
	description = "Node Damage Test Hammer",
	inventory_image = "node_damage_test_hammer.png",

	--When left-clicking, 
	on_use = function(itemstack, user, pointed_thing)
		--If the user is pointing at a node,
		if pointed_thing.type == "node" then
			--Get the position of that node
			local pos = pointed_thing.under

			--Damage it once.
			node_damage.damage(pos, nil, user)
		end
	end,

	--When right-clicking,
	on_place = function(itemstack, placer, pointed_thing)
		--If the user is pointing at a node,
		if pointed_thing.type == "node" then
			--Get the position of that node
			local pos = pointed_thing.under

			--Repair it once.
			node_damage.repair(pos)
		end
	end
})



--If the tnt mod is being used...
if tnt then
	--Create a function that, after blasing a position, cracks nearby nodes.
	local function on_blast(pos, intensity)
		--Blast the position with a 3 node radius.
		tnt.boom(pos, {radius = minetest.settings:get("tnt_radius") or 3})

		--Cracking radius is set to a minimum of 5, but can be bigger if the explosion is more intense.
		local r = 5 * math.max(1, intensity)

		--In an 11x11x11 cube...
		for a = -r, r do
			for b = -r, r do
				for c = -r, r do
					--Get the current pos of the node to damage
					local npos = vector.add(pos, {x=a, y=b, z=c})
					--Get the distance between the current node and the center of the blast
					local dist = vector.distance(pos, npos)
					--Get the number of times to crack this node. Its random,
					--but the maximum increases when its closer to the center of the blast.
					local num = math.random(0, math.ceil(r / math.max(1, dist)))

					--If the distance of this node is within the radius of 5 (to create more of a spherical shape than a cube)
					--And the randomly selected num wasn't 0...
					if dist <= r and num > 0 then
						--Damage this particular node num times.
						node_damage.damage(npos, nil, nil, num)
					end
				end
			end
		end
	end

	--Override tnt blocks to use the above function when exploded.
	minetest.override_item("tnt:tnt", {
		on_blast = function(pos, intensity)
			minetest.after(0.1, on_blast, pos, intensity)
		end
	})

	--Override burning tnt blocks to use the above function once the timer runs out.
	minetest.override_item("tnt:tnt_burning", {
		on_timer = function(pos, elapsed)
			on_blast(pos, 1)
		end
	})
end
