-- Configuration

local chainsaw_max_charge      = 30000 -- Maximum charge of the saw
-- Cut down tree leaves.  Leaf decay may cause slowness on large trees
-- if this is disabled.
local chainsaw_leaves = true

local chainsaw_efficiency = 0.92 -- Drops less items

-- Maximal dimensions of the tree to cut (giant sequoia)
local tree_max_radius = 10
local tree_max_height = 70

local S = technic.getter

--[[
Format: [node_name] = dig_cost

This table is filled automatically afterwards to support mods such as:

	cool_trees
	ethereal
	moretrees
]]
local tree_nodes = {
	-- For the sake of maintenance, keep this sorted alphabetically!
	["default:acacia_bush_stem"] = -1,
	["default:bush_stem"] = -1,
	["default:pine_bush_stem"] = -1,

	["default:cactus"] = -1,
	["default:papyrus"] = -1,

	-- dfcaves "fruits"
	["df_trees:blood_thorn_spike"] = -1,
	["df_trees:blood_thorn_spike_dead"] = -1,
	["df_trees:tunnel_tube_fruiting_body"] = -1,

	["ethereal:bamboo"] = -1,
}

local tree_nodes_by_cid = {
	-- content ID indexed table, data populated on mod load.
	-- Format: [node_name] = cost_number
}

-- Function to decide whether or not to cut a certain node (and at which energy cost)
local function populate_costs(name, def)
	repeat
		if tree_nodes[name] then
			break -- Manually specified node to chop
		end
		if (def.groups.tree or 0) > 0 then
			break -- Tree node
		end
		if (def.groups.leaves or 0) > 0 and chainsaw_leaves then
			break -- Leaves
		end
		if (def.groups.leafdecay_drop or 0) > 0 then
			break -- Food
		end
		return -- Abort function: do not dig this node

	-- luacheck: push ignore 511
	until 1
	-- luacheck: pop

	-- Add the node cost to the content ID indexed table
	local content_id = minetest.get_content_id(name)

	-- Make it so that the giant sequoia can be cut with a full charge
	local cost = tree_nodes[name] or 0
	if def.groups.choppy then
		cost = math.max(cost, def.groups.choppy * 14) -- trunks (usually 3 * 14)
	end
	if def.groups.snappy then
		cost = math.max(cost, def.groups.snappy * 2) -- leaves
	end
	tree_nodes_by_cid[content_id] = math.max(4, cost)
end

minetest.register_on_mods_loaded(function()
	local ndefs = minetest.registered_nodes
	-- Populate hardcoded nodes
	for name in pairs(tree_nodes) do
		local ndef = ndefs[name]
		if ndef and ndef.groups then
			populate_costs(name, ndef)
		end
	end

	-- Find all trees and leaves
	for name, def in pairs(ndefs) do
		if def.groups then
			populate_costs(name, def)
		end
	end
end)


technic.register_power_tool("technic:chainsaw", chainsaw_max_charge)

local pos9dir = {
	{ 1, 0,  0},
	{-1, 0,  0},
	{ 0, 0,  1},
	{ 0, 0, -1},
	{ 1, 0,  1},
	{-1, 0, -1},
	{ 1, 0, -1},
	{-1, 0,  1},
	{ 0, 1,  0}, -- up
}

local cutter = {
	-- See function cut_tree()
}

local c_air = minetest.get_content_id("air")
local function dig_recursive(x, y, z)
	local i = cutter.area:index(x, y, z)
	if cutter.seen[i] then
		return
	end
	cutter.seen[i] = 1 -- Mark as visited

	if cutter.param2[i] ~= 0 then
		-- Do not dig manually placed nodes
		-- Problem: moretrees' generated jungle trees use param2 = 2
		return
	end

	local c_id = cutter.data[i]
	local cost = tree_nodes_by_cid[c_id]
	if not cost or cost > cutter.charge then
		return -- Cannot dig this node
	end

	-- Count dug nodes
	cutter.drops[c_id] = (cutter.drops[c_id] or 0) + 1
	cutter.seen[i] = 2 -- Mark as dug (for callbacks)
	cutter.data[i] = c_air
	cutter.charge = cutter.charge - cost

	-- Expand maximal bounds for area protection check
	if x < cutter.minp.x then cutter.minp.x = x end
	if y < cutter.minp.y then cutter.minp.y = y end
	if z < cutter.minp.z then cutter.minp.z = z end
	if x > cutter.maxp.x then cutter.maxp.x = x end
	if y > cutter.maxp.y then cutter.maxp.y = y end
	if z > cutter.maxp.z then cutter.maxp.z = z end

	-- Traverse neighbors
	local xn, yn, zn
	for _, offset in ipairs(pos9dir) do
		xn, yn, zn = x + offset[1], y + offset[2], z + offset[3]
		if cutter.area:contains(xn, yn, zn) then
			 dig_recursive(xn, yn, zn)
		end
	end
end

local handle_drops

local function chainsaw_dig(player, pos, remaining_charge)
	local minp = {
		x = pos.x - (tree_max_radius + 1),
		y = pos.y,
		z = pos.z - (tree_max_radius + 1)
	}
	local maxp = {
		x = pos.x + (tree_max_radius + 1),
		y = pos.y + tree_max_height,
		z = pos.z + (tree_max_radius + 1)
	}

	local vm = minetest.get_voxel_manip()
	local emin, emax = vm:read_from_map(minp, maxp)

	cutter = {
		area = VoxelArea:new{MinEdge=emin, MaxEdge=emax},
		data = vm:get_data(),
		param2 = vm:get_param2_data(),
		seen = {},
		drops = {}, -- [content_id] = count
		minp = vector.copy(pos),
		maxp = vector.copy(pos),
		charge = remaining_charge
	}

	dig_recursive(pos.x, pos.y, pos.z)

	-- Check protection
	local player_name = player:get_player_name()
	if minetest.is_area_protected(cutter.minp, cutter.maxp, player_name, 6) then
		minetest.chat_send_player(player_name, "The chainsaw cannot cut this tree. The cuboid " ..
			minetest.pos_to_string(cutter.minp) .. ", " .. minetest.pos_to_string(cutter.maxp) ..
			" contains protected nodes.")
		minetest.record_protection_violation(pos, player_name)
		return
	end

	minetest.sound_play("chainsaw", {
		pos = pos,
		gain = 1.0,
		max_hear_distance = 20
	})

	handle_drops(pos)

	vm:set_data(cutter.data)
	vm:write_to_map(true)
	vm:update_map()

	-- Update falling nodes
	for i, status in pairs(cutter.seen) do
		if status == 2 then -- actually dug
			minetest.check_for_falling(cutter.area:position(i))
		end
	end
end

-- Function to randomize positions for new node drops
local function get_drop_pos(pos)
	local drop_pos = {}

	for i = 0, 8 do
		-- Randomize position for a new drop
		drop_pos.x = pos.x + math.random(-3, 3)
		drop_pos.y = pos.y - 1
		drop_pos.z = pos.z + math.random(-3, 3)

		-- Move the randomized position upwards until
		-- the node is air or unloaded.
		for y = drop_pos.y, drop_pos.y + 5 do
			drop_pos.y = y
			local node = minetest.get_node_or_nil(drop_pos)

			if not node then
				-- If the node is not loaded yet simply drop
				-- the item at the original digging position.
				return pos
			elseif node.name == "air" then
				-- Add variation to the entity drop position,
				-- but don't let drops get too close to the edge
				drop_pos.x = drop_pos.x + (math.random() * 0.8) - 0.5
				drop_pos.z = drop_pos.z + (math.random() * 0.8) - 0.5
				return drop_pos
			end
		end
	end

	-- Return the original position if this takes too long
	return pos
end

local drop_inv = minetest.create_detached_inventory("technic:chainsaw_drops", {}, ":technic")
handle_drops = function(pos)
	local n_slots = 100
	drop_inv:set_size("main", n_slots)
	drop_inv:set_list("main", {})

	-- Put all dropped items into the detached inventory
	for c_id, count in pairs(cutter.drops) do
		local name = minetest.get_name_from_content_id(c_id)

		-- Add drops in bulk -> keep some randomness
		while count > 0 do
			local drops = minetest.get_node_drops(name, "")
			-- higher numbers are faster but return uneven sapling counts
			local decrement = math.ceil(count * 0.3)
			decrement = math.min(count, math.max(5, decrement))

			for _, stack in ipairs(drops) do
				stack = ItemStack(stack)
				local total = math.ceil(stack:get_count() * decrement * chainsaw_efficiency)
				local stack_max = stack:get_stack_max()

				-- Split into full stacks
				while total > 0 do
					local size = math.min(total, stack_max)
					stack:set_count(size)
					drop_inv:add_item("main", stack)
					total = total - size
				end
			end
			count = count - decrement
		end
	end

	-- Drop in random places
	for i = 1, n_slots do
		local stack = drop_inv:get_stack("main", i)
		if stack:is_empty() then
			break
		end
		minetest.add_item(get_drop_pos(pos), stack)
	end

	drop_inv:set_size("main", 0) -- free RAM
end


minetest.register_tool("technic:chainsaw", {
	description = S("Chainsaw"),
	inventory_image = "technic_chainsaw.png",
	stack_max = 1,
	wear_represents = "technic_RE_charge",
	on_refill = technic.refill_RE_charge,
	on_use = function(itemstack, user, pointed_thing)
		if pointed_thing.type ~= "node" then
			return itemstack
		end

		local meta = minetest.deserialize(itemstack:get_metadata())
		if not meta or not meta.charge then
			return
		end

		local name = user:get_player_name()
		if minetest.is_protected(pointed_thing.under, name) then
			minetest.record_protection_violation(pointed_thing.under, name)
			return
		end

		-- Send current charge to digging function so that the
		-- chainsaw will stop after digging a number of nodes
		chainsaw_dig(user, pointed_thing.under, meta.charge)
		meta.charge = cutter.charge

		cutter = {} -- Free RAM

		if not technic.creative_mode then
			technic.set_RE_wear(itemstack, meta.charge, chainsaw_max_charge)
			itemstack:set_metadata(minetest.serialize(meta))
		end
		return itemstack
	end,
})

local mesecons_button = minetest.get_modpath("mesecons_button")
local trigger = mesecons_button and "mesecons_button:button_off" or "default:mese_crystal_fragment"

minetest.register_craft({
	output = "technic:chainsaw",
	recipe = {
		{"technic:stainless_steel_ingot", trigger,                      "technic:battery"},
		{"basic_materials:copper_wire",      "basic_materials:motor",              "technic:battery"},
		{"",                              "",                           "technic:stainless_steel_ingot"},
	},
	replacements = { {"basic_materials:copper_wire", "basic_materials:empty_spool"}, },

})

