-- A random assortment of methods used in various places in this mod.

dofile( minetest.get_modpath( "digtron" ) .. "/util_item_place_node.lua" ) -- separated out to avoid potential for license complexity
dofile( minetest.get_modpath( "digtron" ) .. "/util_execute_cycle.lua" ) -- separated out simply for tidiness, there's some big code in there

local node_inventory_table = {type="node"} -- a reusable parameter for get_inventory calls, set the pos parameter before using.

-- Apparently node_sound_metal_defaults is a newer thing, I ran into games using an older version of the default mod without it.
if default.node_sound_metal_defaults ~= nil then
	digtron.metal_sounds = default.node_sound_metal_defaults()
else
	digtron.metal_sounds = default.node_sound_stone_defaults()
end


digtron.find_new_pos = function(pos, facing)
	-- finds the point one node "forward", based on facing
	local dir = minetest.facedir_to_dir(facing)
	return vector.add(pos, dir)
end

local facedir_to_down_dir_table = {
	[0]={x=0, y=-1, z=0},
	{x=0, y=0, z=-1},
	{x=0, y=0, z=1},
	{x=-1, y=0, z=0},
	{x=1, y=0, z=0},
	{x=0, y=1, z=0}
}
digtron.facedir_to_down_dir = function(facing)
	return facedir_to_down_dir_table[math.floor(facing/4)]
end

digtron.find_new_pos_downward = function(pos, facing)
	return vector.add(pos, digtron.facedir_to_down_dir(facing))
end

local registered_nodes = core.registered_nodes
local unknown_node_def_fallback = {
	-- for node_duplicator.lua / on_receive_fields
	description = "unknown node",
	-- for class_layout.lua / get_node_image
	paramtype2 = "none",
}
digtron.get_nodedef = function(name)
	return registered_nodes[name] or unknown_node_def_fallback
end

digtron.mark_diggable = function(pos, nodes_dug, player)
	-- mark the node as dug, if the player provided would have been able to dig it.
	-- Don't *actually* dig the node yet, though, because if we dig a node with sand over it the sand will start falling
	-- and then destroy whatever node we place there subsequently (either by a builder head or by moving a digtron node)
	-- I don't like sand. It's coarse and rough and irritating and it gets everywhere. And it necessitates complicated dig routines.
	-- returns fuel cost and what will be dropped by digging these nodes.

	local target = minetest.get_node(pos)

	-- prevent digtrons from being marked for digging.
	if minetest.get_item_group(target.name, "digtron") ~= 0 or
		minetest.get_item_group(target.name, "digtron_protected") ~= 0 or
		minetest.get_item_group(target.name, "immortal") ~= 0 then
		return 0
	end

	local targetdef = minetest.registered_nodes[target.name]
	if targetdef == nil or targetdef.can_dig == nil or targetdef.can_dig(pos, player) then
		nodes_dug:set(pos.x, pos.y, pos.z, true)
		if target.name ~= "air" then
			local in_known_group = false
			local material_cost = 0

			if digtron.config.uses_resources then
				if minetest.get_item_group(target.name, "cracky") ~= 0 then
					in_known_group = true
					material_cost = math.max(material_cost, digtron.config.dig_cost_cracky)
				end
				if minetest.get_item_group(target.name, "crumbly") ~= 0 then
					in_known_group = true
					material_cost = math.max(material_cost, digtron.config.dig_cost_crumbly)
				end
				if minetest.get_item_group(target.name, "choppy") ~= 0 then
					in_known_group = true
					material_cost = math.max(material_cost, digtron.config.dig_cost_choppy)
				end
				if not in_known_group then
					material_cost = digtron.config.dig_cost_default
				end
			end

			return material_cost, minetest.get_node_drops(target.name, "")
		end
	end
	return 0
end

digtron.can_build_to = function(pos, protected_nodes, dug_nodes)
	-- Returns whether a space is clear to have something put into it

	if protected_nodes:get(pos.x, pos.y, pos.z) then
		return false
	end

	-- tests if the location pointed to is clear to move something into
	local target = minetest.get_node(pos)
	if target.name == "air" or
	   dug_nodes:get(pos.x, pos.y, pos.z) == true or
	   digtron.get_nodedef(target.name).buildable_to
	   then
		return true
	end
	return false
end

digtron.can_move_to = function(pos, protected_nodes, dug_nodes)
	-- Same as can_build_to, but also checks if the current node is part of the digtron.
	-- this allows us to disregard obstructions that *will* move out of the way.
	if digtron.can_build_to(pos, protected_nodes, dug_nodes) == true or
	   minetest.get_item_group(minetest.get_node(pos).name, "digtron") ~= 0 then
		return true
	end
	return false
end


digtron.place_in_inventory = function(itemname, inventory_positions, fallback_pos)
	--tries placing the item in each inventory node in turn. If there's no room, drop it at fallback_pos
	local itemstack = ItemStack(itemname)
	if inventory_positions ~= nil then
		for _, location in pairs(inventory_positions) do
			node_inventory_table.pos = location.pos
			local inv = minetest.get_inventory(node_inventory_table)
			itemstack = inv:add_item("main", itemstack)
			if itemstack:is_empty() then
				return nil
			end
		end
	end
	minetest.add_item(fallback_pos, itemstack)
end

digtron.place_in_specific_inventory = function(itemname, pos, inventory_positions, fallback_pos)
	--tries placing the item in a specific inventory. Other parameters are used as fallbacks on failure
	--Use this method for putting stuff back after testing and failed builds so that if the player
	--is trying to keep various inventories organized manually stuff will go back where it came from,
	--probably.
	local itemstack = ItemStack(itemname)
	node_inventory_table.pos = pos
	local inv = minetest.get_inventory(node_inventory_table)
	local returned_stack = inv:add_item("main", itemstack)
	if not returned_stack:is_empty() then
		-- we weren't able to put the item back into that particular inventory for some reason.
		-- try putting it *anywhere.*
		digtron.place_in_inventory(returned_stack, inventory_positions, fallback_pos)
	end
end

digtron.take_from_inventory = function(itemname, inventory_positions)
	if inventory_positions == nil then return nil end
	--tries to take an item from each inventory node in turn. Returns location of inventory item was taken from on success, nil on failure
	local itemstack = ItemStack(itemname)
	for _, location in pairs(inventory_positions) do
		node_inventory_table.pos = location.pos
		local inv = minetest.get_inventory(node_inventory_table)
		local output = inv:remove_item("main", itemstack)
		if not output:is_empty() then
			return location.pos
		end
	end
	return nil
end

-- Used to determine which coordinate is being checked for periodicity. eg, if the digtron is moving in the z direction, then periodicity is checked for every n nodes in the z axis
digtron.get_controlling_coordinate = function(_, facedir)
	-- used for determining builder period and offset
	local dir = digtron.facedir_to_dir_map[facedir]
	if dir == 1 or dir == 3 then
		return "z"
	elseif dir == 2 or dir == 4 then
		return "x"
	else
		return "y"
	end
end

local fuel_craft = {method="fuel", width=1, items={}} -- reusable crafting recipe table for get_craft_result calls below
-- Searches fuel store inventories for burnable items and burns them until target is reached or surpassed
-- (or there's nothing left to burn). Returns the total fuel value burned
-- if the "test" parameter is set to true, doesn't actually take anything out of inventories.
-- We can get away with this sort of thing for fuel but not for builder inventory because there's just one
-- controller node burning stuff, not multiple build heads drawing from inventories in turn. Much simpler.
digtron.burn = function(fuelstore_positions, target, test)
	if fuelstore_positions == nil then
		return 0
	end

	local current_burned = 0
	for _, location in pairs(fuelstore_positions) do
		if current_burned > target then
			break
		end
		node_inventory_table.pos = location.pos
		local inv = minetest.get_inventory(node_inventory_table)
		local invlist = inv:get_list("fuel")

		if invlist == nil then -- This check shouldn't be needed, it's yet another guard against https://github.com/minetest/minetest/issues/8067
			break
		end

		for _, itemstack in pairs(invlist) do
			fuel_craft.items[1] = itemstack:peek_item(1)
			local fuel_per_item = minetest.get_craft_result(fuel_craft).time
			if fuel_per_item ~= 0 then
				local actual_burned = math.min(
						math.ceil((target - current_burned)/fuel_per_item), -- burn this many, if we can.
						itemstack:get_count() -- how many we have at most.
					)
				if test ~= true then
					-- don't bother recording the items if we're just testing, nothing is actually being removed.
					itemstack:set_count(itemstack:get_count() - actual_burned)
				end
				current_burned = current_burned + actual_burned * fuel_per_item
			end
			if current_burned > target then
				break
			end
		end
		if test ~= true then
			-- only update the list if we're doing this for real.
			inv:set_list("fuel", invlist)
		end
	end
	return current_burned
end

-- Consume energy from the batteries
-- The same as burning coal, except that instead of destroying the items in the inventory, we merely drain
-- the charge in them, leaving them empty. The charge is converted into "coal heat units" by a downscaling
-- factor, since if taken at face value (10000 EU), the batteries would be the ultimate power source barely
-- ever needing replacement.
digtron.tap_batteries = function(battery_positions, target, test)
	if (battery_positions == nil) then
		return 0
	end

	local current_burned = 0
	-- 1 coal block is 370 PU
	-- 1 coal lump is 40 PU
	-- An RE battery holds 10000 EU of charge
	-- local power_ratio = 100 -- How much charge equals 1 unit of PU from coal
	-- setting Moved to digtron.config.power_ratio

	for _, location in pairs(battery_positions) do
		if current_burned > target then
			break
		end
		node_inventory_table.pos = location.pos
		local inv = minetest.get_inventory(node_inventory_table)
		local invlist = inv:get_list("batteries")

		if (invlist == nil) then -- This check shouldn't be needed, it's yet another guard against https://github.com/minetest/minetest/issues/8067
			break
		end

		for _, itemstack in pairs(invlist) do
			local meta = minetest.deserialize(itemstack:get_metadata())
			if (meta ~= nil) then
				local power_available = math.floor(meta.charge / digtron.config.power_ratio)
				if power_available ~= 0 then
					local actual_burned = power_available -- we just take all we have from the battery, since they aren't stackable
					-- don't bother recording the items if we're just testing, nothing is actually being removed.
					if test ~= true then
						-- since we are taking everything, the wear and charge can both be set to 0
						itemstack:set_wear(0)
						meta.charge = 0
						itemstack:set_metadata(minetest.serialize(meta))
					end
					current_burned = current_burned + actual_burned
				end
			end

			if current_burned > target then
				break
			end
		end

		if test ~= true then
			-- only update the list if we're doing this for real.
			inv:set_list("batteries", invlist)
		end
	end
	return current_burned
end

digtron.remove_builder_item = function(pos)
	local objects = minetest.get_objects_inside_radius(pos, 0.5)
	if objects ~= nil then
		for _, obj in ipairs(objects) do
			if obj and obj:get_luaentity() and obj:get_luaentity().name == "digtron:builder_item" then
				obj:remove()
			end
		end
	end
end

digtron.update_builder_item = function(pos)
	digtron.remove_builder_item(pos)
	node_inventory_table.pos = pos
	local inv = minetest.get_inventory(node_inventory_table)
	if inv == nil or inv:get_size("main") < 1 then return end
	local item_stack = inv:get_stack("main", 1)
	if not item_stack:is_empty() then
		digtron.create_builder_item = item_stack:get_name()
		minetest.add_entity(pos,"digtron:builder_item")
	end
end

local damage_def = {
	full_punch_interval = 1.0,
	damage_groups = {},
}
digtron.damage_creatures = function(player, source_pos, target_pos, amount, items_dropped)
	if type(player) ~= 'userdata' then
		return
	end
	local objects = minetest.get_objects_inside_radius(target_pos, 1.0)
	if objects ~= nil then
		damage_def.damage_groups.fleshy = amount
		local velocity = {
			x = target_pos.x-source_pos.x,
			y = target_pos.y-source_pos.y + 0.2,
			z = target_pos.z-source_pos.z,
		}
		for _, obj in ipairs(objects) do
			if obj:is_player() then
				-- Digtron moving logic handles owner movement
				if obj:get_player_name() ~= player:get_player_name() then
					-- See issue #2960 for status of a "set player velocity" method
					-- instead, knock the player back
					local newpos = {
						x = target_pos.x + velocity.x,
						y = target_pos.y + velocity.y,
						z = target_pos.z + velocity.z,
					}
					obj:set_pos(newpos)
					obj:punch(player, 1.0, damage_def, nil)
				end
			else
				local lua_entity = obj:get_luaentity()
				if lua_entity ~= nil then
					if lua_entity.name == "__builtin:item" then
						table.insert(items_dropped, lua_entity.itemstring)
						lua_entity.itemstring = ""
						obj:remove()
					elseif lua_entity.name == "__builtin:falling_node" then
						-- Eat all falling nodes in front of the digtron
						-- to avoid them eating away our digger heads
						local drops = minetest.get_node_drops(lua_entity.node, "")
						for _, item in pairs(drops) do
							table.insert(items_dropped, item)
						end
						obj:remove()
					else
						if obj.add_velocity ~= nil then
							obj:add_velocity(velocity)
						else
							local vel = obj:get_velocity()
							obj:set_velocity(vector.add(vel, velocity))
						end
						obj:punch(player, 1.0, damage_def, nil)
					end
				end
			end
		end
	end
	-- If we killed any mobs they might have dropped some stuff, vacuum that up now too.
	objects = minetest.get_objects_inside_radius(target_pos, 1.0)
	if objects ~= nil then
		for _, obj in ipairs(objects) do
			if not obj:is_player() then
				local lua_entity = obj:get_luaentity()
				if lua_entity ~= nil and lua_entity.name == "__builtin:item" then
					table.insert(items_dropped, lua_entity.itemstring)
					lua_entity.itemstring = ""
					obj:remove()
				end
			end
		end
	end
end

digtron.is_soft_material = function(target)
	local target_node = minetest.get_node(target)
	if  minetest.get_item_group(target_node.name, "crumbly") ~= 0 or
		minetest.get_item_group(target_node.name, "choppy") ~= 0 or
		minetest.get_item_group(target_node.name, "snappy") ~= 0 or
		minetest.get_item_group(target_node.name, "oddly_breakable_by_hand") ~= 0 or
		minetest.get_item_group(target_node.name, "fleshy") ~= 0 then
		return true
	end
	return false
end

-- If someone sets very large offsets or intervals for the offset markers they might be added too far
-- away. safe_add_entity causes these attempts to be ignored rather than crashing the game.
-- returns the entity if successful, nil otherwise
local function safe_add_entity(pos, name)
	local success, ret = pcall(minetest.add_entity, pos, name)
	if success then return ret else return nil end
end

digtron.show_offset_markers = function(pos, offset, period)
	local buildpos = digtron.find_new_pos(pos, minetest.get_node(pos).param2)
	local x_pos = math.floor((buildpos.x+offset)/period)*period - (offset or 0)
	safe_add_entity({x=x_pos, y=buildpos.y, z=buildpos.z}, "digtron:marker")
	if x_pos >= buildpos.x then
		safe_add_entity({x=x_pos - period, y=buildpos.y, z=buildpos.z}, "digtron:marker")
	end
	if x_pos <= buildpos.x then
		safe_add_entity({x=x_pos + period, y=buildpos.y, z=buildpos.z}, "digtron:marker")
	end

	local y_pos = math.floor((buildpos.y+offset)/period)*period - offset
	safe_add_entity({x=buildpos.x, y=y_pos, z=buildpos.z}, "digtron:marker_vertical")
	if y_pos >= buildpos.y then
		safe_add_entity({x=buildpos.x, y=y_pos - period, z=buildpos.z}, "digtron:marker_vertical")
	end
	if y_pos <= buildpos.y then
		safe_add_entity({x=buildpos.x, y=y_pos + period, z=buildpos.z}, "digtron:marker_vertical")
	end

	local z_pos = math.floor((buildpos.z+offset)/period)*period - offset

	local entity = safe_add_entity({x=buildpos.x, y=buildpos.y, z=z_pos}, "digtron:marker")
	if entity ~= nil then entity:set_yaw(1.5708) end

	if z_pos >= buildpos.z then
		entity = safe_add_entity({x=buildpos.x, y=buildpos.y, z=z_pos - period}, "digtron:marker")
		if entity ~= nil then entity:set_yaw(1.5708) end
	end
	if z_pos <= buildpos.z then
		entity = safe_add_entity({x=buildpos.x, y=buildpos.y, z=z_pos + period}, "digtron:marker")
		if entity ~= nil then entity:set_yaw(1.5708) end
	end
end

digtron.check_protected_and_record = function(pos, player)
	local name = player:get_player_name()
	if minetest.is_protected(pos, name) then
		minetest.record_protection_violation(pos, name)
		return true
	end
	return false
end

digtron.protected_allow_metadata_inventory_put = function(pos, _, _, stack, player)
	return digtron.check_protected_and_record(pos, player) and 0 or stack:get_count()
end

digtron.protected_allow_metadata_inventory_move = function(pos, _, _, _, _, count, player)
	return digtron.check_protected_and_record(pos, player) and 0 or count
end

digtron.protected_allow_metadata_inventory_take = function(pos, _, _, stack, player)
	return digtron.check_protected_and_record(pos, player) and 0 or stack:get_count()
end